.history page refined
This commit is contained in:
@@ -510,15 +510,15 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
|||||||
backend.CompleteDownloadItem(itemID, filename, 0)
|
backend.CompleteDownloadItem(itemID, filename, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
go func(fPath, track, artist, album, sID, cover, format string) {
|
go func(fPath, track, artist, album, sID, cover, format, source string) {
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
quality := "Unknown"
|
quality := "Unknown"
|
||||||
durationStr := "--:--"
|
durationStr := "0:00"
|
||||||
|
|
||||||
meta, err := backend.GetTrackMetadata(fPath)
|
meta, err := backend.GetTrackMetadata(fPath)
|
||||||
if err == nil && meta != nil {
|
if err == nil {
|
||||||
if meta.BitsPerSample > 0 {
|
if meta.Bitrate > 0 {
|
||||||
quality = fmt.Sprintf("%d-bit/%.1fkHz", meta.BitsPerSample, float64(meta.SampleRate)/1000.0)
|
|
||||||
} else if meta.Bitrate > 0 {
|
|
||||||
quality = fmt.Sprintf("%dkbps/%.1fkHz", meta.Bitrate/1000, float64(meta.SampleRate)/1000.0)
|
quality = fmt.Sprintf("%dkbps/%.1fkHz", meta.Bitrate/1000, float64(meta.SampleRate)/1000.0)
|
||||||
} else if meta.SampleRate > 0 {
|
} else if meta.SampleRate > 0 {
|
||||||
quality = fmt.Sprintf("%.1fkHz", float64(meta.SampleRate)/1000.0)
|
quality = fmt.Sprintf("%.1fkHz", float64(meta.SampleRate)/1000.0)
|
||||||
@@ -539,6 +539,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
|||||||
Quality: quality,
|
Quality: quality,
|
||||||
Format: strings.ToUpper(format),
|
Format: strings.ToUpper(format),
|
||||||
Path: fPath,
|
Path: fPath,
|
||||||
|
Source: source,
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.Format == "" || item.Format == "LOSSLESS" {
|
if item.Format == "" || item.Format == "LOSSLESS" {
|
||||||
@@ -554,7 +555,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
backend.AddHistoryItem(item, "SpotiFLAC")
|
backend.AddHistoryItem(item, "SpotiFLAC")
|
||||||
}(filename, req.TrackName, req.ArtistName, req.AlbumName, req.SpotifyID, req.CoverURL, req.AudioFormat)
|
}(filename, req.TrackName, req.ArtistName, req.AlbumName, req.SpotifyID, req.CoverURL, req.AudioFormat, req.Service)
|
||||||
}
|
}
|
||||||
|
|
||||||
return DownloadResponse{
|
return DownloadResponse{
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type HistoryItem struct {
|
|||||||
Quality string `json:"quality"`
|
Quality string `json:"quality"`
|
||||||
Format string `json:"format"`
|
Format string `json:"format"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
|
Source string `json:"source"`
|
||||||
Timestamp int64 `json:"timestamp"`
|
Timestamp int64 `json:"timestamp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-19
@@ -555,7 +555,7 @@ func FilterTrack(data map[string]interface{}, separator string, albumFetchData .
|
|||||||
copyrightData := getMap(albumData, "copyright")
|
copyrightData := getMap(albumData, "copyright")
|
||||||
if len(copyrightData) > 0 {
|
if len(copyrightData) > 0 {
|
||||||
copyrightItems := getSlice(copyrightData, "items")
|
copyrightItems := getSlice(copyrightData, "items")
|
||||||
if copyrightItems != nil {
|
if len(copyrightItems) > 0 {
|
||||||
for _, item := range copyrightItems {
|
for _, item := range copyrightItems {
|
||||||
itemMap, ok := item.(map[string]interface{})
|
itemMap, ok := item.(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -574,7 +574,7 @@ func FilterTrack(data map[string]interface{}, separator string, albumFetchData .
|
|||||||
if len(tracksData) > 0 {
|
if len(tracksData) > 0 {
|
||||||
discNumbers := make(map[int]bool)
|
discNumbers := make(map[int]bool)
|
||||||
trackItems := getSlice(tracksData, "items")
|
trackItems := getSlice(tracksData, "items")
|
||||||
if trackItems != nil {
|
if len(trackItems) > 0 {
|
||||||
for _, item := range trackItems {
|
for _, item := range trackItems {
|
||||||
itemMap, ok := item.(map[string]interface{})
|
itemMap, ok := item.(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -656,7 +656,7 @@ func FilterTrack(data map[string]interface{}, separator string, albumFetchData .
|
|||||||
|
|
||||||
albumArtistsString := ""
|
albumArtistsString := ""
|
||||||
albumLabel := ""
|
albumLabel := ""
|
||||||
if albumFetchDataMap != nil && len(albumFetchDataMap) > 0 {
|
if len(albumFetchDataMap) > 0 {
|
||||||
albumUnionData := getMap(getMap(albumFetchDataMap, "data"), "albumUnion")
|
albumUnionData := getMap(getMap(albumFetchDataMap, "data"), "albumUnion")
|
||||||
if len(albumUnionData) > 0 {
|
if len(albumUnionData) > 0 {
|
||||||
albumArtists := extractArtists(getMap(albumUnionData, "artists"))
|
albumArtists := extractArtists(getMap(albumUnionData, "artists"))
|
||||||
@@ -957,21 +957,9 @@ func FilterPlaylist(data map[string]interface{}, separator string) map[string]in
|
|||||||
avatarData := getMap(ownerData, "avatar")
|
avatarData := getMap(ownerData, "avatar")
|
||||||
if len(avatarData) > 0 {
|
if len(avatarData) > 0 {
|
||||||
sources := getSlice(avatarData, "sources")
|
sources := getSlice(avatarData, "sources")
|
||||||
if sources != nil {
|
if len(sources) > 0 {
|
||||||
for _, source := range sources {
|
if firstSource, ok := sources[0].(map[string]interface{}); ok {
|
||||||
sourceMap, ok := source.(map[string]interface{})
|
avatarURL = getString(firstSource, "url")
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if getFloat64(sourceMap, "width") == 300 {
|
|
||||||
avatarURL = getString(sourceMap, "url")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if avatarURL == nil && len(sources) > 0 {
|
|
||||||
if firstSource, ok := sources[0].(map[string]interface{}); ok {
|
|
||||||
avatarURL = getString(firstSource, "url")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1291,7 +1279,7 @@ func extractDiscographyItems(itemsData map[string]interface{}) []map[string]inte
|
|||||||
}
|
}
|
||||||
|
|
||||||
func stripHTMLTags(s string) string {
|
func stripHTMLTags(s string) string {
|
||||||
re := regexp.MustCompile(`<[^>]*>`)
|
re := regexp.MustCompile(`(?s)<[^>]*>`)
|
||||||
return re.ReplaceAllString(s, "")
|
return re.ReplaceAllString(s, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, Pagi
|
|||||||
import { GetDownloadHistory, ClearDownloadHistory, GetPreviewURL, GetFetchHistory, DeleteDownloadHistoryItem, DeleteFetchHistoryItem, ClearFetchHistoryByType } from "../../wailsjs/go/main/App";
|
import { GetDownloadHistory, ClearDownloadHistory, GetPreviewURL, GetFetchHistory, DeleteDownloadHistoryItem, DeleteFetchHistoryItem, ClearFetchHistoryByType } from "../../wailsjs/go/main/App";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { openExternal } from "@/lib/utils";
|
import { openExternal } from "@/lib/utils";
|
||||||
|
import { TidalIcon, QobuzIcon, AmazonIcon } from "./PlatformIcons";
|
||||||
const formatDate = (timestamp: number) => {
|
const formatDate = (timestamp: number) => {
|
||||||
const date = new Date(timestamp * 1000);
|
const date = new Date(timestamp * 1000);
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
@@ -30,6 +31,7 @@ interface DownloadHistoryItem {
|
|||||||
quality: string;
|
quality: string;
|
||||||
format: string;
|
format: string;
|
||||||
path: string;
|
path: string;
|
||||||
|
source: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
interface FetchHistoryItem {
|
interface FetchHistoryItem {
|
||||||
@@ -62,10 +64,35 @@ export function HistoryPage({ onHistorySelect }: HistoryPageProps) {
|
|||||||
const [fetchSearchQuery, setFetchSearchQuery] = useState("");
|
const [fetchSearchQuery, setFetchSearchQuery] = useState("");
|
||||||
const [fetchCurrentPage, setFetchCurrentPage] = useState(1);
|
const [fetchCurrentPage, setFetchCurrentPage] = useState(1);
|
||||||
const ITEMS_PER_PAGE = 50;
|
const ITEMS_PER_PAGE = 50;
|
||||||
|
const getTrackLink = (spotifyId: string) => {
|
||||||
|
if (spotifyId?.startsWith("tidal_"))
|
||||||
|
return { url: `https://listen.tidal.com/track/${spotifyId.replace("tidal_", "")}`, label: "Open in Tidal" };
|
||||||
|
if (spotifyId?.startsWith("qobuz_"))
|
||||||
|
return { url: `https://www.qobuz.com/track/${spotifyId.replace("qobuz_", "")}`, label: "Open in Qobuz" };
|
||||||
|
if (spotifyId?.startsWith("amazon_"))
|
||||||
|
return { url: `https://music.amazon.com/tracks/${spotifyId.replace("amazon_", "")}`, label: "Open in Amazon Music" };
|
||||||
|
if (spotifyId?.startsWith("deezer_"))
|
||||||
|
return { url: `https://www.deezer.com/track/${spotifyId.replace("deezer_", "")}`, label: "Open in Deezer" };
|
||||||
|
return { url: `https://open.spotify.com/track/${spotifyId}`, label: "Open in Spotify" };
|
||||||
|
};
|
||||||
|
const getSourceIcon = (source: string) => {
|
||||||
|
const s = source?.toLowerCase() || "";
|
||||||
|
if (s.includes("tidal"))
|
||||||
|
return <TidalIcon className="h-4 w-4 object-contain rounded"/>;
|
||||||
|
if (s.includes("qobuz"))
|
||||||
|
return <QobuzIcon className="h-4 w-4 object-contain"/>;
|
||||||
|
if (s.includes("amazon"))
|
||||||
|
return <AmazonIcon className="h-4 w-4 object-contain rounded"/>;
|
||||||
|
if (s.includes("deezer"))
|
||||||
|
return <Music2 className="h-4 w-4"/>;
|
||||||
|
if (s.includes("spotify"))
|
||||||
|
return <Music2 className="h-4 w-4"/>;
|
||||||
|
return <Music2 className="h-4 w-4 opacity-50"/>;
|
||||||
|
};
|
||||||
const fetchDownloadHistory = async () => {
|
const fetchDownloadHistory = async () => {
|
||||||
try {
|
try {
|
||||||
const items = await GetDownloadHistory();
|
const items = await GetDownloadHistory();
|
||||||
setDownloadHistory(items || []);
|
setDownloadHistory((items || []) as unknown as DownloadHistoryItem[]);
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.error("Failed to fetch download history:", err);
|
console.error("Failed to fetch download history:", err);
|
||||||
@@ -228,8 +255,8 @@ export function HistoryPage({ onHistorySelect }: HistoryPageProps) {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h2 className="text-xl font-bold tracking-tight">Downloads</h2>
|
<h2 className="text-xl font-bold tracking-tight">Downloads</h2>
|
||||||
{downloadHistory.length > 0 && (<Badge variant="secondary" className="font-mono">
|
{filteredDownloadHistory.length > 0 && (<Badge variant="secondary" className="font-mono">
|
||||||
{downloadHistory.length.toLocaleString('en-US')}
|
{filteredDownloadHistory.length.toLocaleString('en-US')}
|
||||||
</Badge>)}
|
</Badge>)}
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm" onClick={() => setShowClearDownloadConfirm(true)} disabled={downloadHistory.length === 0} className="cursor-pointer gap-2">
|
<Button variant="outline" size="sm" onClick={() => setShowClearDownloadConfirm(true)} disabled={downloadHistory.length === 0} className="cursor-pointer gap-2">
|
||||||
@@ -275,11 +302,12 @@ export function HistoryPage({ onHistorySelect }: HistoryPageProps) {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b bg-muted/50">
|
<tr className="border-b bg-muted/50">
|
||||||
<th className="h-10 px-4 text-left align-middle font-medium text-muted-foreground w-12 text-xs uppercase">#</th>
|
<th className="h-10 px-4 text-left align-middle font-medium text-muted-foreground w-12 text-xs uppercase">#</th>
|
||||||
<th className="h-10 px-4 text-left align-middle font-medium text-muted-foreground text-xs uppercase">Title</th>
|
<th className="h-10 px-4 text-left align-middle font-medium text-muted-foreground text-xs uppercase w-[35%]">Title</th>
|
||||||
<th className="h-10 px-4 text-left align-middle font-medium text-muted-foreground hidden md:table-cell text-xs uppercase w-1/4">Album</th>
|
<th className="h-10 px-4 text-left align-middle font-medium text-muted-foreground hidden md:table-cell text-xs uppercase w-48 lg:w-48 xl:w-56">Album</th>
|
||||||
<th className="h-10 px-4 text-left align-middle font-medium text-muted-foreground hidden lg:table-cell w-32 text-xs uppercase">Format</th>
|
<th className="h-10 px-4 text-left align-middle font-medium text-muted-foreground hidden lg:table-cell w-32 text-xs uppercase">Format</th>
|
||||||
<th className="h-10 px-4 text-left align-middle font-medium text-muted-foreground hidden xl:table-cell w-16 text-xs uppercase text-nowrap">Dur</th>
|
<th className="h-10 px-4 text-left align-middle font-medium text-muted-foreground hidden xl:table-cell w-16 text-xs uppercase text-nowrap">Dur</th>
|
||||||
<th className="h-10 px-4 text-left align-middle font-medium text-muted-foreground hidden md:table-cell w-36 text-xs uppercase text-nowrap">Downloaded At</th>
|
<th className="h-10 px-4 text-left align-middle font-medium text-muted-foreground hidden md:table-cell w-36 text-xs uppercase text-nowrap">Downloaded At</th>
|
||||||
|
<th className="h-10 px-4 text-center align-middle font-medium text-muted-foreground w-16 text-xs uppercase text-nowrap">Source</th>
|
||||||
<th className="h-10 px-4 text-center align-middle font-medium text-muted-foreground w-32 text-xs uppercase text-nowrap">Actions</th>
|
<th className="h-10 px-4 text-center align-middle font-medium text-muted-foreground w-32 text-xs uppercase text-nowrap">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -311,36 +339,52 @@ export function HistoryPage({ onHistorySelect }: HistoryPageProps) {
|
|||||||
<td className="p-3 align-middle text-sm text-muted-foreground text-left hidden xl:table-cell font-mono">
|
<td className="p-3 align-middle text-sm text-muted-foreground text-left hidden xl:table-cell font-mono">
|
||||||
{item.duration_str}
|
{item.duration_str}
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3 align-middle text-xs text-muted-foreground hidden md:table-cell whitespace-nowrap">
|
<td className="p-3 align-middle text-xs text-muted-foreground hidden md:table-cell whitespace-nowrap text-left">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span>{formatDate(item.timestamp).split(' ')[0]}</span>
|
<span>{formatDate(item.timestamp).split(' ')[0]}</span>
|
||||||
<span className="text-[10px] text-muted-foreground">{formatDate(item.timestamp).split(' ')[1]}</span>
|
<span className="text-[10px] text-muted-foreground">{formatDate(item.timestamp).split(' ')[1]}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3 align-middle text-center">
|
<td className="p-3 align-middle text-center">
|
||||||
<div className="flex items-center justify-center gap-1">
|
<div className="flex items-center justify-center">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip delayDuration={0}>
|
<Tooltip delayDuration={0}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8 cursor-pointer" onClick={() => handlePreview(item.id, item.spotify_id)} disabled={!item.spotify_id}>
|
<div className="flex items-center justify-center">
|
||||||
{playingPreviewId === item.id ? <Pause className="h-4 w-4"/> : <Play className="h-4 w-4"/>}
|
{getSourceIcon(item.source)}
|
||||||
</Button>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>{playingPreviewId === item.id ? "Pause Preview" : "Play Preview"}</p>
|
<p className="capitalize">{item.source || "Unknown"}</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="p-3 align-middle text-center">
|
||||||
|
<div className="flex items-center justify-center gap-1">
|
||||||
|
{!(item.spotify_id?.startsWith('tidal_') || item.spotify_id?.startsWith('qobuz_') || item.spotify_id?.startsWith('amazon_') || item.spotify_id?.startsWith('deezer_')) && (<TooltipProvider>
|
||||||
|
<Tooltip delayDuration={0}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8 cursor-pointer" onClick={() => handlePreview(item.id, item.spotify_id)} disabled={!item.spotify_id}>
|
||||||
|
{playingPreviewId === item.id ? <Pause className="h-4 w-4"/> : <Play className="h-4 w-4"/>}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{playingPreviewId === item.id ? "Pause Preview" : "Play Preview"}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>)}
|
||||||
|
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip delayDuration={0}>
|
<Tooltip delayDuration={0}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8 cursor-pointer" onClick={() => openExternal(`https://open.spotify.com/track/${item.spotify_id}`)}>
|
<Button variant="ghost" size="icon" className="h-8 w-8 cursor-pointer" onClick={() => openExternal(getTrackLink(item.spotify_id).url)}>
|
||||||
<ExternalLink className="h-4 w-4"/>
|
<ExternalLink className="h-4 w-4"/>
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Open in Spotify</p>
|
<p>{getTrackLink(item.spotify_id).label}</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
Reference in New Issue
Block a user