This commit is contained in:
afkarxyz
2026-01-15 13:20:04 +07:00
parent b160d3c790
commit 1e99d8b5c6
5 changed files with 323 additions and 153 deletions
+17 -1
View File
@@ -714,6 +714,17 @@ func FilterTrack(data map[string]interface{}, albumFetchData ...map[string]inter
} }
} }
if albumArtistsString == "" {
albumArtists := extractArtists(getMap(albumData, "artists"))
if len(albumArtists) > 0 {
albumArtistNames := []string{}
for _, artist := range albumArtists {
albumArtistNames = append(albumArtistNames, getString(artist, "name"))
}
albumArtistsString = strings.Join(albumArtistNames, ", ")
}
}
albumInfo = map[string]interface{}{ albumInfo = map[string]interface{}{
"id": albumID, "id": albumID,
"name": getString(albumData, "name"), "name": getString(albumData, "name"),
@@ -1228,6 +1239,11 @@ func extractDiscographyItems(itemsData map[string]interface{}) []map[string]inte
return items return items
} }
func stripHTMLTags(s string) string {
re := regexp.MustCompile(`<[^>]*>`)
return re.ReplaceAllString(s, "")
}
func FilterArtist(data map[string]interface{}) map[string]interface{} { func FilterArtist(data map[string]interface{}) map[string]interface{} {
dataMap := getMap(data, "data") dataMap := getMap(data, "data")
artistData := getMap(dataMap, "artistUnion") artistData := getMap(dataMap, "artistUnion")
@@ -1243,7 +1259,7 @@ func FilterArtist(data map[string]interface{}) map[string]interface{} {
if ok { if ok {
biographyText := getString(biographyMap, "text") biographyText := getString(biographyMap, "text")
if biographyText != "" { if biographyText != "" {
profile["biography"] = html.UnescapeString(biographyText) profile["biography"] = html.UnescapeString(stripHTMLTags(biographyText))
} }
} }
} }
+18
View File
@@ -0,0 +1,18 @@
export const langColors: Record<string, string> = {
"TypeScript": "#2b7489",
"Go": "#375eab",
"Python": "#3572A5",
"CSS": "#563d7c",
"HTML": "#e44b23",
"JavaScript": "#f1e05a",
"Java": "#b07219",
"C": "#555555",
"C Sharp": "#178600",
"cpp": "#f34b7d",
"Ruby": "#701516",
"PHP": "#4F5D95",
"Swift": "#ffac45",
"Kotlin": "#F18E33",
"Rust": "#dea584",
"Shell": "#89e051"
};
+134 -2
View File
@@ -8,11 +8,12 @@ import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { Bug, Lightbulb, ExternalLink } from "lucide-react"; import { Bug, Lightbulb, ExternalLink, Star, GitFork, Clock, Download } from "lucide-react";
import ExyezedIcon from "@/assets/icons/exyezed.svg"; import ExyezedIcon from "@/assets/icons/exyezed.svg";
import SpotubeDLIcon from "@/assets/icons/spotubedl.svg"; import SpotubeDLIcon from "@/assets/icons/spotubedl.svg";
import SpotiDownloaderIcon from "@/assets/icons/spotidownloader.svg"; import SpotiDownloaderIcon from "@/assets/icons/spotidownloader.svg";
import XBatchDLIcon from "@/assets/icons/xbatchdl.svg"; import XBatchDLIcon from "@/assets/icons/xbatchdl.svg";
import { langColors } from "@/assets/github-lang-colors";
interface AboutPageProps { interface AboutPageProps {
version: string; version: string;
} }
@@ -27,6 +28,7 @@ export function AboutPage({ version }: AboutPageProps) {
const [featureDesc, setFeatureDesc] = useState(""); const [featureDesc, setFeatureDesc] = useState("");
const [useCase, setUseCase] = useState(""); const [useCase, setUseCase] = useState("");
const [featureContext, setFeatureContext] = useState(""); const [featureContext, setFeatureContext] = useState("");
const [repoStats, setRepoStats] = useState<Record<string, any>>({});
useEffect(() => { useEffect(() => {
const fetchOS = async () => { const fetchOS = async () => {
try { try {
@@ -66,6 +68,80 @@ export function AboutPage({ version }: AboutPageProps) {
} }
}; };
fetchLocation(); fetchLocation();
const fetchRepoStats = async () => {
const CACHE_KEY = 'github_repo_stats';
const CACHE_DURATION = 1000 * 60 * 60;
const cached = localStorage.getItem(CACHE_KEY);
if (cached) {
try {
const { data, timestamp } = JSON.parse(cached);
if (Date.now() - timestamp < CACHE_DURATION) {
setRepoStats(data);
return;
}
}
catch (err) {
console.error('Failed to parse cache:', err);
}
}
const repos = [
{ name: 'SpotiDownloader', owner: 'afkarxyz' },
{ name: 'Twitter-X-Media-Batch-Downloader', owner: 'afkarxyz' }
];
const stats: Record<string, any> = {};
for (const repo of repos) {
try {
const [repoRes, releasesRes, langsRes] = await Promise.all([
fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}`),
fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/releases`),
fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/languages`)
]);
if (repoRes.status === 403) {
if (cached) {
const { data } = JSON.parse(cached);
setRepoStats(data);
}
return;
}
if (repoRes.ok && releasesRes.ok && langsRes.ok) {
const repoData = await repoRes.json();
const releases = await releasesRes.json();
const languages = await langsRes.json();
let totalDownloads = 0;
let latestDownloads = 0;
if (releases.length > 0) {
latestDownloads = releases[0].assets?.reduce((sum: number, asset: any) => sum + (asset.download_count || 0), 0) || 0;
totalDownloads = releases.reduce((sum: number, release: any) => {
return sum + (release.assets?.reduce((s: number, a: any) => s + (a.download_count || 0), 0) || 0);
}, 0);
}
const topLangs = Object.entries(languages)
.sort(([, a]: any, [, b]: any) => b - a)
.slice(0, 4)
.map(([lang]) => lang);
stats[repo.name] = {
stars: repoData.stargazers_count,
forks: repoData.forks_count,
createdAt: repoData.created_at,
totalDownloads,
latestDownloads,
languages: topLangs
};
}
}
catch (err) {
console.error(`Failed to fetch stats for ${repo.name}:`, err);
if (cached) {
const { data } = JSON.parse(cached);
setRepoStats(data);
return;
}
}
}
setRepoStats(stats);
localStorage.setItem(CACHE_KEY, JSON.stringify({ data: stats, timestamp: Date.now() }));
};
fetchRepoStats();
}, []); }, []);
const faqs = [ const faqs = [
{ {
@@ -92,6 +168,34 @@ export function AboutPage({ version }: AboutPageProps) {
const sanitizeForURL = (text: string): string => { const sanitizeForURL = (text: string): string => {
return text.replace(/[()]/g, "").replace(/,/g, " -"); return text.replace(/[()]/g, "").replace(/,/g, " -");
}; };
const formatTimeAgo = (dateString: string): string => {
const now = new Date();
const updated = new Date(dateString);
const diffMs = now.getTime() - updated.getTime();
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
const diffMonths = Math.floor(diffDays / 30);
if (diffDays === 0)
return 'today';
if (diffDays === 1)
return '1d';
if (diffDays < 30)
return `${diffDays}d`;
if (diffMonths === 1)
return '1mo';
if (diffMonths < 12)
return `${diffMonths}mo`;
const diffYears = Math.floor(diffMonths / 12);
return `${diffYears}y`;
};
const formatNumber = (num: number): string => {
if (num >= 1000) {
return num.toLocaleString();
}
return num.toString();
};
const getLangColor = (lang: string): string => {
return langColors[lang] || '#858585';
};
const handleSubmit = () => { const handleSubmit = () => {
let title = ""; let title = "";
let body = ""; let body = "";
@@ -256,12 +360,40 @@ ${location || "Unknown"}
<CardTitle className="flex items-center gap-2"><img src={SpotiDownloaderIcon} className="h-5 w-5" alt="SpotiDownloader" /> SpotiDownloader</CardTitle> <CardTitle className="flex items-center gap-2"><img src={SpotiDownloaderIcon} className="h-5 w-5" alt="SpotiDownloader" /> SpotiDownloader</CardTitle>
<CardDescription>Get Spotify tracks in MP3 and FLAC via the spotidownloader.com API.</CardDescription> <CardDescription>Get Spotify tracks in MP3 and FLAC via the spotidownloader.com API.</CardDescription>
</CardHeader> </CardHeader>
{repoStats['SpotiDownloader'] && (<CardContent className="space-y-3">
<div className="flex flex-wrap gap-2 text-xs">
{repoStats['SpotiDownloader'].languages?.map((lang: string) => (<span key={lang} className="px-2 py-0.5 rounded-full font-medium" style={{ backgroundColor: getLangColor(lang) + '20', color: getLangColor(lang) }}>{lang}</span>))}
</div>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1"><Star className="h-3.5 w-3.5 fill-amber-500 text-amber-500" /> {formatNumber(repoStats['SpotiDownloader'].stars)}</span>
<span className="flex items-center gap-1"><GitFork className="h-3.5 w-3.5" /> {repoStats['SpotiDownloader'].forks}</span>
<span className="flex items-center gap-1"><Clock className="h-3.5 w-3.5" /> {formatTimeAgo(repoStats['SpotiDownloader'].createdAt)}</span>
</div>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1"><Download className="h-3.5 w-3.5" /> TOTAL: {formatNumber(repoStats['SpotiDownloader'].totalDownloads)}</span>
<span className="flex items-center gap-1 text-green-600 dark:text-green-400"><Download className="h-3.5 w-3.5" /> LATEST: {formatNumber(repoStats['SpotiDownloader'].latestDownloads)}</span>
</div>
</CardContent>)}
</Card> </Card>
<Card className="hover:bg-muted/50 hover:border-primary/50 transition-colors cursor-pointer" onClick={() => openExternal("https://github.com/afkarxyz/Twitter-X-Media-Batch-Downloader")}> <Card className="hover:bg-muted/50 hover:border-primary/50 transition-colors cursor-pointer" onClick={() => openExternal("https://github.com/afkarxyz/Twitter-X-Media-Batch-Downloader")}>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"><img src={XBatchDLIcon} className="h-5 w-5" alt="Twitter/X Media Batch Downloader" /> Twitter/X Batch Downloader</CardTitle> <CardTitle className="flex items-center gap-2"><img src={XBatchDLIcon} className="h-5 w-5" alt="Twitter/X Media Batch Downloader" /> Twitter/X Media Batch Downloader</CardTitle>
<CardDescription>A GUI tool to download original-quality images and videos from Twitter/X accounts, powered by gallery-dl by @mikf</CardDescription> <CardDescription>A GUI tool to download original-quality images and videos from Twitter/X accounts, powered by gallery-dl by @mikf</CardDescription>
</CardHeader> </CardHeader>
{repoStats['Twitter-X-Media-Batch-Downloader'] && (<CardContent className="space-y-3">
<div className="flex flex-wrap gap-2 text-xs">
{repoStats['Twitter-X-Media-Batch-Downloader'].languages?.map((lang: string) => (<span key={lang} className="px-2 py-0.5 rounded-full font-medium" style={{ backgroundColor: getLangColor(lang) + '20', color: getLangColor(lang) }}>{lang}</span>))}
</div>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1"><Star className="h-3.5 w-3.5 fill-amber-500 text-amber-500" /> {formatNumber(repoStats['Twitter-X-Media-Batch-Downloader'].stars)}</span>
<span className="flex items-center gap-1"><GitFork className="h-3.5 w-3.5" /> {repoStats['Twitter-X-Media-Batch-Downloader'].forks}</span>
<span className="flex items-center gap-1"><Clock className="h-3.5 w-3.5" /> {formatTimeAgo(repoStats['Twitter-X-Media-Batch-Downloader'].createdAt)}</span>
</div>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1"><Download className="h-3.5 w-3.5" /> TOTAL: {formatNumber(repoStats['Twitter-X-Media-Batch-Downloader'].totalDownloads)}</span>
<span className="flex items-center gap-1 text-green-600 dark:text-green-400"><Download className="h-3.5 w-3.5" /> LATEST: {formatNumber(repoStats['Twitter-X-Media-Batch-Downloader'].latestDownloads)}</span>
</div>
</CardContent>)}
</Card> </Card>
</div> </div>
</TabsContent> </TabsContent>
+8 -4
View File
@@ -209,8 +209,10 @@ export function HistoryPage() {
</td> </td>
<td className="p-3 align-middle text-left hidden lg:table-cell"> <td className="p-3 align-middle text-left hidden lg:table-cell">
<div className="flex flex-col items-start gap-1"> <div className="flex flex-col items-start gap-1">
<span className="text-[10px] font-bold text-foreground">{item.format}</span> <span className="text-xs font-bold text-foreground">
{item.quality && <span className="text-[9px] text-muted-foreground leading-none whitespace-nowrap">{item.quality}</span>} {['HI_RES_LOSSLESS', 'LOSSLESS'].includes(item.format) ? 'FLAC' : item.format}
</span>
{item.quality && <span className="text-[11px] text-muted-foreground leading-none whitespace-nowrap">{item.quality}</span>}
</div> </div>
</td> </td>
<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">
@@ -229,7 +231,8 @@ export function HistoryPage() {
</table>)} </table>)}
</div> </div>
{totalPages > 1 && (<Pagination> {
totalPages > 1 && (<Pagination>
<PaginationContent> <PaginationContent>
<PaginationItem> <PaginationItem>
<PaginationPrevious href="#" onClick={(e) => { <PaginationPrevious href="#" onClick={(e) => {
@@ -258,7 +261,8 @@ export function HistoryPage() {
}} className={currentPage === totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"} /> }} className={currentPage === totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"} />
</PaginationItem> </PaginationItem>
</PaginationContent> </PaginationContent>
</Pagination>)} </Pagination>)
}
<Dialog open={showClearConfirm} onOpenChange={setShowClearConfirm}> <Dialog open={showClearConfirm} onOpenChange={setShowClearConfirm}>
<DialogContent className="max-w-md [&>button]:hidden"> <DialogContent className="max-w-md [&>button]:hidden">
+12 -12
View File
@@ -266,40 +266,40 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest }: Setting
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="tidal-qobuz"> <SelectItem value="tidal-qobuz">
<span className="flex items-center gap-1.5"><TidalIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><QobuzIcon className="fill-white"/></span> <span className="flex items-center gap-1.5"><TidalIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><QobuzIcon className="fill-current"/></span>
</SelectItem> </SelectItem>
<SelectItem value="tidal-amazon"> <SelectItem value="tidal-amazon">
<span className="flex items-center gap-1.5"><TidalIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><AmazonIcon className="fill-white"/></span> <span className="flex items-center gap-1.5"><TidalIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><AmazonIcon className="fill-current"/></span>
</SelectItem> </SelectItem>
<SelectItem value="qobuz-tidal"> <SelectItem value="qobuz-tidal">
<span className="flex items-center gap-1.5"><QobuzIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><TidalIcon className="fill-white"/></span> <span className="flex items-center gap-1.5"><QobuzIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><TidalIcon className="fill-current"/></span>
</SelectItem> </SelectItem>
<SelectItem value="qobuz-amazon"> <SelectItem value="qobuz-amazon">
<span className="flex items-center gap-1.5"><QobuzIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><AmazonIcon className="fill-white"/></span> <span className="flex items-center gap-1.5"><QobuzIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><AmazonIcon className="fill-current"/></span>
</SelectItem> </SelectItem>
<SelectItem value="amazon-tidal"> <SelectItem value="amazon-tidal">
<span className="flex items-center gap-1.5"><AmazonIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><TidalIcon className="fill-white"/></span> <span className="flex items-center gap-1.5"><AmazonIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><TidalIcon className="fill-current"/></span>
</SelectItem> </SelectItem>
<SelectItem value="amazon-qobuz"> <SelectItem value="amazon-qobuz">
<span className="flex items-center gap-1.5"><AmazonIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><QobuzIcon className="fill-white"/></span> <span className="flex items-center gap-1.5"><AmazonIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><QobuzIcon className="fill-current"/></span>
</SelectItem> </SelectItem>
<SelectItem value="tidal-qobuz-amazon"> <SelectItem value="tidal-qobuz-amazon">
<span className="flex items-center gap-1.5"><TidalIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><QobuzIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><AmazonIcon className="fill-white"/></span> <span className="flex items-center gap-1.5"><TidalIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><QobuzIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><AmazonIcon className="fill-current"/></span>
</SelectItem> </SelectItem>
<SelectItem value="tidal-amazon-qobuz"> <SelectItem value="tidal-amazon-qobuz">
<span className="flex items-center gap-1.5"><TidalIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><AmazonIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><QobuzIcon className="fill-white"/></span> <span className="flex items-center gap-1.5"><TidalIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><AmazonIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><QobuzIcon className="fill-current"/></span>
</SelectItem> </SelectItem>
<SelectItem value="qobuz-tidal-amazon"> <SelectItem value="qobuz-tidal-amazon">
<span className="flex items-center gap-1.5"><QobuzIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><TidalIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><AmazonIcon className="fill-white"/></span> <span className="flex items-center gap-1.5"><QobuzIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><TidalIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><AmazonIcon className="fill-current"/></span>
</SelectItem> </SelectItem>
<SelectItem value="qobuz-amazon-tidal"> <SelectItem value="qobuz-amazon-tidal">
<span className="flex items-center gap-1.5"><QobuzIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><AmazonIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><TidalIcon className="fill-white"/></span> <span className="flex items-center gap-1.5"><QobuzIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><AmazonIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><TidalIcon className="fill-current"/></span>
</SelectItem> </SelectItem>
<SelectItem value="amazon-tidal-qobuz"> <SelectItem value="amazon-tidal-qobuz">
<span className="flex items-center gap-1.5"><AmazonIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><TidalIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><QobuzIcon className="fill-white"/></span> <span className="flex items-center gap-1.5"><AmazonIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><TidalIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><QobuzIcon className="fill-current"/></span>
</SelectItem> </SelectItem>
<SelectItem value="amazon-qobuz-tidal"> <SelectItem value="amazon-qobuz-tidal">
<span className="flex items-center gap-1.5"><AmazonIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><QobuzIcon className="fill-white"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><TidalIcon className="fill-white"/></span> <span className="flex items-center gap-1.5"><AmazonIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><QobuzIcon className="fill-current"/><ArrowRight className="h-3 w-3 text-muted-foreground"/><TidalIcon className="fill-current"/></span>
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>