This commit is contained in:
Eduard Prigoana 2025-08-22 04:42:18 +03:00
parent be789cb732
commit c23eb924c3
85 changed files with 7090 additions and 253 deletions

View file

@ -1,31 +1,35 @@
# archive.py
import logging import logging
from waybackpy import WaybackMachineSaveAPI
import time
import random import random
import time
from typing import List
from waybackpy import WaybackMachineSaveAPI
from config import ARCHIVE_URLS, USER_AGENT from config import ARCHIVE_URLS, USER_AGENT
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def archive_url(url):
def archive_url(url: str):
logger.info(f"🌐 Archiving {url} ...") logger.info(f"🌐 Archiving {url} ...")
try: try:
save_api = WaybackMachineSaveAPI(url, user_agent=USER_AGENT) save_api = WaybackMachineSaveAPI(url, user_agent=USER_AGENT)
save_api.save() save_api.save()
logger.info(f"✅ Archived {url}") logger.info(f"✅ Archived {url}")
except Exception as e: except Exception as e:
logger.error(f"⚠️ Exception archiving {url}: {e}") logger.error(f"⚠️ Exception archiving {url}: {e}", exc_info=True)
def archive_all_urls(): def archive_all_urls():
logger.info("--- Starting archival process for all URLs ---")
for url in ARCHIVE_URLS: for url in ARCHIVE_URLS:
delay = 10 + random.uniform(-3, 3) delay = 10 + random.uniform(-3, 3)
logger.info(f"Waiting {delay:.2f} seconds before next archive...")
time.sleep(delay) time.sleep(delay)
archive_url(url) archive_url(url)
logger.info("--- Archival process finished ---")
def test_archive(): def test_archive():
test_url = "https://httpbin.org/anything/foo/bar" test_url = "https://httpbin.org/anything/foo/bar"

View file

@ -11,6 +11,7 @@ CSV_FILENAME = "artists.csv"
XLSX_FILENAME = "artists.xlsx" XLSX_FILENAME = "artists.xlsx"
exclude_names = { exclude_names = {
"🎹Worst Comps & Edits"
"K4$H K4$$!n0", "K4$H K4$$!n0",
"K4HKn0", "K4HKn0",
"AI Models", "AI Models",

47
diff.py
View file

@ -1,22 +1,37 @@
# diff.py
import csv import csv
import logging
from typing import Dict, List
def read_csv_to_dict(filename): logger = logging.getLogger(__name__)
d = {}
with open(filename, newline='', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
d[row["Artist Name"]] = row
return d
def detect_changes(old_data, new_data):
def read_csv_to_dict(filename: str) -> Dict[str, Dict[str, str]]:
data = {}
try:
with open(filename, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
if "Artist Name" in row and row["Artist Name"]:
data[row["Artist Name"]] = row
except FileNotFoundError:
logger.warning(f"CSV file not found: {filename}")
except Exception as e:
logger.error(f"Error reading CSV file {filename}: {e}", exc_info=True)
return data
def detect_changes(
old_data: Dict[str, Dict[str, str]], new_data: Dict[str, Dict[str, str]]
) -> List[str]:
changes = [] changes = []
old_keys = set(old_data.keys()) old_keys = set(old_data.keys())
new_keys = set(new_data.keys()) new_keys = set(new_data.keys())
removed = old_keys - new_keys removed = sorted(list(old_keys - new_keys))
added = new_keys - old_keys added = sorted(list(new_keys - old_keys))
common = old_keys & new_keys common = sorted(list(old_keys & new_keys))
for artist in removed: for artist in removed:
changes.append(f"❌ Removed: **{artist}**") changes.append(f"❌ Removed: **{artist}**")
@ -28,15 +43,15 @@ def detect_changes(old_data, new_data):
old_row = old_data[artist] old_row = old_data[artist]
new_row = new_data[artist] new_row = new_data[artist]
if old_row["URL"] != new_row["URL"]: if old_row.get("URL") != new_row.get("URL"):
changes.append(f"🔗 Link changed for **{artist}**") changes.append(f"🔗 Link changed for **{artist}**")
if old_row["Credit"] != new_row["Credit"]: if old_row.get("Credit") != new_row.get("Credit"):
changes.append(f"✏️ Credit changed for **{artist}**") changes.append(f"✏️ Credit changed for **{artist}**")
if old_row["Links Work"] != new_row["Links Work"]: if old_row.get("Links Work") != new_row.get("Links Work"):
changes.append(f"🔄 Links Work status changed for **{artist}**") changes.append(f"🔄 Links Work status changed for **{artist}**")
if old_row["Updated"] != new_row["Updated"]: if old_row.get("Updated") != new_row.get("Updated"):
changes.append(f"🕒 Updated date changed for **{artist}**") changes.append(f"🕒 Updated date changed for **{artist}**")
if old_row["Best"] != new_row["Best"]: if old_row.get("Best") != new_row.get("Best"):
changes.append(f"⭐ Best flag changed for **{artist}**") changes.append(f"⭐ Best flag changed for **{artist}**")
return changes return changes

View file

@ -1,36 +1,42 @@
import requests, zipfile # downloader.py
import logging
import zipfile
import requests
from config import HTML_FILENAME, XLSX_FILENAME, XLSX_URL, ZIP_FILENAME, ZIP_URL
logger = logging.getLogger(__name__)
def _download_file(url: str, filename: str, timeout: int = 30) -> bool:
logger.info(f"🔄 Downloading {filename}...")
try:
with requests.get(url, timeout=timeout) as r:
r.raise_for_status()
with open(filename, "wb") as f:
f.write(r.content)
logger.info(f"✅ Saved {filename}")
return True
except requests.RequestException as e:
logger.error(f"❌ Failed to download {filename}: {e}")
return False
from config import ZIP_URL, ZIP_FILENAME, HTML_FILENAME, XLSX_URL, XLSX_FILENAME
def download_zip_and_extract_html(): def download_zip_and_extract_html():
print("🔄 Downloading ZIP...") if not _download_file(ZIP_URL, ZIP_FILENAME):
try:
with requests.get(ZIP_URL, timeout=30) as r:
r.raise_for_status()
with open(ZIP_FILENAME, "wb") as f:
f.write(r.content)
print(f"✅ Saved ZIP as {ZIP_FILENAME}")
except requests.RequestException as e:
print(f"❌ Failed to download ZIP: {e}")
return return
logger.info(f"📦 Extracting {HTML_FILENAME} from {ZIP_FILENAME}...")
try: try:
with zipfile.ZipFile(ZIP_FILENAME, "r") as z: with zipfile.ZipFile(ZIP_FILENAME, "r") as z:
with z.open(HTML_FILENAME) as html_file: html_content = z.read(HTML_FILENAME)
html_content = html_file.read() with open(HTML_FILENAME, "wb") as f:
with open(HTML_FILENAME, "wb") as f: f.write(html_content)
f.write(html_content) logger.info(f"✅ Extracted {HTML_FILENAME}")
print(f"✅ Extracted {HTML_FILENAME}") except (zipfile.BadZipFile, KeyError, FileNotFoundError) as e:
except (zipfile.BadZipFile, KeyError) as e: logger.error(f"❌ Failed to extract {HTML_FILENAME}: {e}")
print(f"❌ Failed to extract {HTML_FILENAME}: {e}")
def download_xlsx(): def download_xlsx():
print("🔄 Downloading XLSX...") _download_file(XLSX_URL, XLSX_FILENAME)
try:
with requests.get(XLSX_URL, timeout=30) as r:
r.raise_for_status()
with open(XLSX_FILENAME, "wb") as f:
f.write(r.content)
print(f"✅ Saved XLSX as {XLSX_FILENAME}")
except requests.RequestException as e:
print(f"❌ Failed to download XLSX: {e}")

View file

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View file

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1,122 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View file

@ -0,0 +1,37 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "ArtistGrid Sheets",
description: "We pull from TrackerHub and parse it into a CSV file. Still a work in progress.",
icons: {
icon: "/favicon.png", // make sure favicon.png is in /public
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}

View file

@ -0,0 +1,68 @@
"use client";
import { Button } from "@/components/ui/button";
import { Github, FileDown, FileText, FileSpreadsheet } from "lucide-react";
const buttonData = [
{
name: "View on GitHub",
href: "https://github.com/ArtistGrid/Sheets",
icon: Github,
isExternal: true,
},
{
name: "Download CSV",
href: "https://sheets.artistgrid.cx/artists.csv",
icon: FileDown,
downloadName: "artists.csv",
},
{
name: "View HTML",
href: "https://sheets.artistgrid.cx/artists.html",
icon: FileText,
isExternal: true,
},
{
name: "Download XLSX",
href: "https://sheets.artistgrid.cx/artists.xlsx",
icon: FileSpreadsheet,
downloadName: "ArtistGrid.xlsx",
},
];
export default function ArtistGridSheets() {
return (
<div className="min-h-screen bg-black text-white flex items-center justify-center p-4 sm:p-6">
<div className="w-full max-w-lg text-center bg-neutral-950 border border-neutral-800 rounded-2xl p-8 sm:p-12 shadow-2xl shadow-black/30 animate-in fade-in-0 zoom-in-95 duration-500">
<h1 className="text-3xl sm:text-4xl font-bold bg-gradient-to-b from-neutral-50 to-neutral-400 bg-clip-text text-transparent mb-4">
ArtistGrid Sheets
</h1>
<p className="text-neutral-400 mb-10 max-w-sm mx-auto">
We pull from TrackerHub and parse it into a CSV file. Still a work in
progress.
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{buttonData.map((button) => (
<Button
key={button.name}
asChild
className="bg-white text-black hover:bg-neutral-200 font-semibold rounded-lg h-14 text-base transition-all duration-300 ease-out hover:-translate-y-1 hover:shadow-[0_0_30px_rgba(255,255,255,0.3)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-black focus-visible:ring-white"
>
<a
href={button.href}
{...(button.isExternal && {
target: "_blank",
rel: "noopener noreferrer",
})}
{...(button.downloadName && { download: button.downloadName })}
>
<button.icon className="w-5 h-5 mr-2.5" aria-hidden="true" />
{button.name}
</a>
</Button>
))}
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View file

@ -0,0 +1,59 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View file

@ -0,0 +1,25 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
{
ignores: [
"node_modules/**",
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
],
},
];
export default eslintConfig;

View file

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View file

@ -0,0 +1,17 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
output: 'export',
// Optional: Change links `/me` -> `/me/` and emit `/me.html` -> `/me/index.html`
// trailingSlash: true,
// Optional: Prevent automatic `/me` -> `/me/`, instead preserve `href`
// skipTrailingSlashRedirect: true,
// Optional: Change the output directory `out` -> `dist`
// distDir: 'dist',
}
module.exports = nextConfig

View file

@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,33 @@
{
"name": "artistgrid-sheets-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@radix-ui/react-slot": "^1.2.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.540.0",
"next": "15.5.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.5.0",
"tailwindcss": "^4",
"tw-animate-css": "^1.3.7",
"typescript": "^5"
}
}

View file

@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

View file

@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

View file

@ -1,15 +1,15 @@
{ {
"last_updated": "2025-08-19T05:14:17.304876Z", "last_updated": "2025-08-22T01:41:42.228132+00:00",
"files": { "files": {
"Artists.html": { "Artists.html": {
"hash": "b48e3d341500e82e179f8813443e5fed899313b48ef79b83bd87388189c49a90", "hash": "6ddd64a83ad530c441bcfd88007c923878a4bd529f07ea60947b1185b8cc1879",
"last_archived": "2025-08-19T05:14:17.304886Z" "last_archived": "2025-08-19T05:14:17.304886Z"
}, },
"artists.csv": { "artists.csv": {
"hash": "609ab1ad6adf62bc1ab8e1b433e1676ab6787e65c760e22d7da7a8e26bbdf33e" "hash": "01e5dd552b219b3472ad95a27660add9e9b47a868ed62bde4f1f303e0e1a514d"
}, },
"artists.xlsx": { "artists.xlsx": {
"hash": "101b6cdef091774668b030bd1bed27a78cb227b487f5f04883c7a2f4bb184b5e" "hash": "69d0f7146e1aa4ce9ab3080bec2cc0ed1696b5ea3f289296a58d00f19ed5812f"
} }
} }
} }

145
main.py
View file

@ -1,96 +1,115 @@
from flask import Flask, send_file, send_from_directory, jsonify # main.py
from flask_cors import CORS
import threading
import os
import json import json
import logging
import os
import threading
from config import HTML_FILENAME, CSV_FILENAME, XLSX_FILENAME from flask import Flask, jsonify, send_file, send_from_directory
from flask_cors import CORS
from config import CSV_FILENAME, HTML_FILENAME, XLSX_FILENAME
from update_loop import update_loop from update_loop import update_loop
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
app = Flask(__name__) app = Flask(__name__)
CORS(app) CORS(app)
# Serve main files
@app.route("/")
def serve_index():
return send_file("templates/index.html")
@app.route("/artists.html") @app.route("/artists.html")
def serve_artists_html(): def serve_artists_html():
return send_file(HTML_FILENAME, mimetype="text/html") return send_file(HTML_FILENAME)
@app.route("/artists.csv") @app.route("/artists.csv")
def serve_artists_csv(): def serve_artists_csv():
return send_file(CSV_FILENAME, mimetype="text/csv") return send_file(CSV_FILENAME)
@app.route("/artists.xlsx") @app.route("/artists.xlsx")
def serve_artists_xlsx(): def serve_artists_xlsx():
return send_file(XLSX_FILENAME, mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") return send_file(XLSX_FILENAME)
# Serve index and frontend assets
@app.route("/")
@app.route("/index")
@app.route("/index.html")
def serve_index():
return send_file("templates/index.html", mimetype="text/html")
@app.route("/_next/<path:filename>") @app.route("/_next/<path:filename>")
def serve_next_static(filename): def serve_next_static(filename):
return send_from_directory("templates/_next", filename) return send_from_directory("templates/_next", filename)
# Serve /info JSON
def get_status_data():
info_path = os.path.join("info", "status.json")
if not os.path.exists(info_path):
return None
try:
with open(info_path, "r") as f:
return json.load(f)
except (IOError, json.JSONDecodeError) as e:
logger.error(f"Failed to read or parse status.json: {e}")
return None
@app.route("/info") @app.route("/info")
def info_json(): def info_json():
info_path = os.path.join("info", "status.json") data = get_status_data()
if os.path.exists(info_path): if data:
with open(info_path) as f: return jsonify(data)
return jsonify(json.load(f)) return jsonify({"error": "Info not available"}), 404
return {"error": "Info not available"}, 404
# Serve /info HTML
@app.route("/info/html") @app.route("/info/html")
def info_html(): def info_html():
info_path = os.path.join("info", "status.json") data = get_status_data()
if os.path.exists(info_path): if not data:
with open(info_path) as f: return "<p>Status info not available.</p>", 404
data = json.load(f)
html = f""" files_info = data.get("files", {})
<html> html_info = files_info.get(HTML_FILENAME, {})
<head><title>File Info</title></head> csv_info = files_info.get(CSV_FILENAME, {})
<body> xlsx_info = files_info.get(XLSX_FILENAME, {})
<h1>Latest File Info</h1>
<p><strong>Last Updated:</strong> {data.get('last_updated')}</p> return f"""
<ul> <!DOCTYPE html>
<li><strong>Artists.html</strong><br> <html lang="en">
Hash: {data['files']['Artists.html']['hash']}<br> <head>
Archived: {data['files']['Artists.html']['last_archived']} <meta charset="UTF-8">
</li> <title>File Info</title>
<li><strong>artists.csv</strong><br> <style>body {{ font-family: sans-serif; }} li {{ margin-bottom: 1em; }}</style>
Hash: {data['files']['artists.csv']['hash']} </head>
</li> <body>
<li><strong>artists.xlsx</strong><br> <h1>Latest File Info</h1>
Hash: {data['files']['artists.xlsx']['hash']} <p><strong>Last Updated:</strong> {data.get('last_updated', 'N/A')}</p>
</li> <ul>
</ul> <li><strong>{HTML_FILENAME}</strong><br>
</body> Hash: {html_info.get('hash', 'N/A')}<br>
</html> Archived: {html_info.get('last_archived', 'N/A')}
""" </li>
return html <li><strong>{CSV_FILENAME}</strong><br>
return "<p>Status info not available.</p>", 404 Hash: {csv_info.get('hash', 'N/A')}
</li>
<li><strong>{XLSX_FILENAME}</strong><br>
Hash: {xlsx_info.get('hash', 'N/A')}
</li>
</ul>
</body>
</html>
"""
# 404 page
@app.errorhandler(404) @app.errorhandler(404)
def page_not_found(e): def page_not_found(e):
return send_file("templates/404.html", mimetype="text/html"), 404 return send_file("templates/404.html"), 404
# Start app and updater
if __name__ == "__main__": if __name__ == "__main__":
# Run update loop in background logger.info("Starting background update thread...")
threading.Thread(target=update_loop, daemon=True).start() threading.Thread(target=update_loop, daemon=True).start()
logger.info("Starting Flask server...")
# Optional: perform initial download/generation if needed
from downloader import download_zip_and_extract_html, download_xlsx
from parser import generate_csv
# Uncomment below if you want to do initial sync before serving
# download_zip_and_extract_html()
# download_xlsx()
# generate_csv()
app.run(host="0.0.0.0", port=5000) app.run(host="0.0.0.0", port=5000)

View file

@ -1,20 +1,30 @@
import os, json, requests # notify.py
import json
import logging
import requests
from config import DISCORD_WEBHOOK_URL from config import DISCORD_WEBHOOK_URL
def send_discord_message(content): logger = logging.getLogger(__name__)
def send_discord_message(content: str):
if not DISCORD_WEBHOOK_URL: if not DISCORD_WEBHOOK_URL:
print("⚠️ Discord webhook URL not set in env") logger.warning("Discord webhook URL not set. Skipping notification.")
return return
if len(content) > 2000:
content = content[:1990] + "\n... (truncated)"
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
data = {"content": content} data = {"content": content}
try: try:
resp = requests.post(DISCORD_WEBHOOK_URL, headers=headers, data=json.dumps(data), timeout=10) response = requests.post(
if resp.status_code in (200, 204): DISCORD_WEBHOOK_URL, headers=headers, data=json.dumps(data), timeout=10
print("✅ Discord notification sent") )
else: response.raise_for_status()
print(f"⚠️ Failed to send Discord notification, status code {resp.status_code}") logger.info("✅ Discord notification sent successfully.")
except Exception as e: except requests.RequestException as e:
print(f"⚠️ Exception sending Discord notification: {e}") logger.error(f"⚠️ Exception sending Discord notification: {e}")

View file

@ -1,50 +1,70 @@
from bs4 import BeautifulSoup # parser.py
import csv import csv
import logging
from config import HTML_FILENAME, CSV_FILENAME, exclude_names from bs4 import BeautifulSoup
from config import CSV_FILENAME, HTML_FILENAME, exclude_names
from utils import clean_artist_name, force_star_flag from utils import clean_artist_name, force_star_flag
logger = logging.getLogger(__name__)
def generate_csv(): def generate_csv():
print("📝 Generating CSV...") logger.info(f"📝 Generating {CSV_FILENAME} from {HTML_FILENAME}...")
with open(HTML_FILENAME, "r", encoding="utf-8") as f: try:
soup = BeautifulSoup(f, "html.parser") with open(HTML_FILENAME, "r", encoding="utf-8") as f:
soup = BeautifulSoup(f, "html.parser")
except FileNotFoundError:
logger.error(f"{HTML_FILENAME} not found. Cannot generate CSV.")
return
rows = soup.select("table.waffle tbody tr")[3:] table_body = soup.select_one("table.waffle tbody")
if not table_body:
logger.error("❌ Could not find the table body in HTML. Cannot generate CSV.")
return
rows = table_body.select("tr")
data = [] data = []
starring = True starring_section = True
for row in rows: for row in rows[3:]:
cells = row.find_all("td") cells = row.find_all("td")
if len(cells) < 4: if len(cells) < 4:
continue continue
# Always take the artist name from the column text
artist_name_raw = cells[0].get_text(strip=True) artist_name_raw = cells[0].get_text(strip=True)
# Only use the <a> for the URL (if it exists)
link_tag = cells[0].find("a") link_tag = cells[0].find("a")
artist_url = link_tag["href"] if link_tag else "" artist_url = link_tag.get("href") if link_tag else ""
if not artist_url:
if not artist_name_raw or not artist_url:
continue continue
if "AI Models" in artist_name_raw: if "AI Models" in artist_name_raw:
starring = False starring_section = False
artist_name_clean = clean_artist_name(artist_name_raw) artist_name_clean = clean_artist_name(artist_name_raw)
if artist_name_clean in exclude_names or "🚩" in artist_name_raw: if artist_name_clean in exclude_names or "🚩" in artist_name_raw:
continue continue
best = force_star_flag(starring) data.append(
credit = cells[1].get_text(strip=True) [
updated = cells[2].get_text(strip=True) artist_name_clean,
links_work = cells[3].get_text(strip=True) artist_url,
cells[1].get_text(strip=True),
cells[3].get_text(strip=True),
cells[2].get_text(strip=True),
force_star_flag(starring_section),
]
)
data.append([artist_name_clean, artist_url, credit, links_work, updated, best]) try:
with open(CSV_FILENAME, "w", newline="", encoding="utf-8") as csvfile:
with open(CSV_FILENAME, "w", newline='', encoding="utf-8") as csvfile: writer = csv.writer(csvfile, quoting=csv.QUOTE_ALL)
writer = csv.writer(csvfile, quoting=csv.QUOTE_ALL) writer.writerow(
writer.writerow(["Artist Name", "URL", "Credit", "Links Work", "Updated", "Best"]) ["Artist Name", "URL", "Credit", "Links Work", "Updated", "Best"]
writer.writerows(data) )
writer.writerows(data)
print(f"✅ CSV saved as {CSV_FILENAME}") logger.info(f"✅ Generated {CSV_FILENAME} with {len(data)} rows.")
except IOError as e:
logger.error(f"❌ Failed to write CSV file {CSV_FILENAME}: {e}")

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
self.__BUILD_MANIFEST=function(e,r,t){return{__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},__routerFilterStatic:{numItems:2,errorRate:1e-4,numBits:39,numHashes:14,bitArray:[0,1,1,0,r,e,e,r,r,e,e,r,e,e,e,r,r,e,e,e,e,r,e,r,r,r,r,e,e,e,r,e,r,e,r,e,e,e,r]},__routerFilterDynamic:{numItems:r,errorRate:1e-4,numBits:r,numHashes:null,bitArray:[]},"/_error":["static/chunks/pages/_error-71d2b6a7b832d02a.js"],sortedPages:["/_app","/_error"]}}(1,0,1e-4),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();

View file

@ -0,0 +1 @@
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[139],{5139:(e,t,l)=>{Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"default",{enumerable:!0,get:function(){return s}});let u=l(4252),n=l(7876),a=u._(l(4232)),o=l(1033);async function r(e){let{Component:t,ctx:l}=e;return{pageProps:await (0,o.loadGetInitialProps)(t,l)}}class s extends a.default.Component{render(){let{Component:e,pageProps:t}=this.props;return(0,n.jsx)(e,{...t})}}s.origGetInitialProps=r,s.getInitialProps=r,("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[472],{472:(e,t,l)=>{Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"default",{enumerable:!0,get:function(){return s}});let u=l(4252),n=l(7876),a=u._(l(4232)),o=l(2746);async function r(e){let{Component:t,ctx:l}=e;return{pageProps:await (0,o.loadGetInitialProps)(t,l)}}class s extends a.default.Component{render(){let{Component:e,pageProps:t}=this.props;return(0,n.jsx)(e,{...t})}}s.origGetInitialProps=r,s.getInitialProps=r,("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[492],{2474:(e,t,l)=>{(window.__NEXT_P=window.__NEXT_P||[]).push(["/_not-found/page",function(){return l(9520)}])},4585:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"styles",{enumerable:!0,get:function(){return l}});let l={error:{fontFamily:'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',height:"100vh",textAlign:"center",display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center"},desc:{display:"inline-block"},h1:{display:"inline-block",margin:"0 20px 0 0",padding:"0 23px 0 0",fontSize:24,fontWeight:500,verticalAlign:"top",lineHeight:"49px"},h2:{fontSize:14,fontWeight:400,lineHeight:"49px",margin:0}};("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)},8886:(e,t,l)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"HTTPAccessErrorFallback",{enumerable:!0,get:function(){return o}});let r=l(5155),n=l(4585);function o(e){let{status:t,message:l}=e;return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)("title",{children:t+": "+l}),(0,r.jsx)("div",{style:n.styles.error,children:(0,r.jsxs)("div",{children:[(0,r.jsx)("style",{dangerouslySetInnerHTML:{__html:"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}),(0,r.jsx)("h1",{className:"next-error-h1",style:n.styles.h1,children:t}),(0,r.jsx)("div",{style:n.styles.desc,children:(0,r.jsx)("h2",{style:n.styles.h2,children:l})})]})})]})}("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)},9520:(e,t,l)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"default",{enumerable:!0,get:function(){return o}});let r=l(5155),n=l(8886),o=function(){return(0,r.jsx)("html",{children:(0,r.jsx)("body",{children:(0,r.jsx)(n.HTTPAccessErrorFallback,{status:404,message:"This page could not be found."})})})};("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)}},e=>{e.O(0,[441,255,358],()=>e(e.s=2474)),_N_E=e.O()}]);

View file

@ -1 +0,0 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[492],{3632:(e,t,r)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"default",{enumerable:!0,get:function(){return o}});let l=r(5155),n=r(6395);function o(){return(0,l.jsx)(n.HTTPAccessErrorFallback,{status:404,message:"This page could not be found."})}("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)},3868:(e,t,r)=>{(window.__NEXT_P=window.__NEXT_P||[]).push(["/_not-found/page",function(){return r(3632)}])},6395:(e,t,r)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"HTTPAccessErrorFallback",{enumerable:!0,get:function(){return o}}),r(8229);let l=r(5155);r(2115);let n={error:{fontFamily:'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',height:"100vh",textAlign:"center",display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center"},desc:{display:"inline-block"},h1:{display:"inline-block",margin:"0 20px 0 0",padding:"0 23px 0 0",fontSize:24,fontWeight:500,verticalAlign:"top",lineHeight:"49px"},h2:{fontSize:14,fontWeight:400,lineHeight:"49px",margin:0}};function o(e){let{status:t,message:r}=e;return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)("title",{children:t+": "+r}),(0,l.jsx)("div",{style:n.error,children:(0,l.jsxs)("div",{children:[(0,l.jsx)("style",{dangerouslySetInnerHTML:{__html:"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}),(0,l.jsx)("h1",{className:"next-error-h1",style:n.h1,children:t}),(0,l.jsx)("div",{style:n.desc,children:(0,l.jsx)("h2",{style:n.h2,children:r})})]})})]})}("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)}},e=>{var t=t=>e(e.s=t);e.O(0,[441,684,358],()=>t(3868)),_N_E=e.O()}]);

View file

@ -1 +0,0 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[492],{3632:(e,t,r)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"default",{enumerable:!0,get:function(){return o}});let l=r(5155),n=r(6395);function o(){return(0,l.jsx)(n.HTTPAccessErrorFallback,{status:404,message:"This page could not be found."})}("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)},3868:(e,t,r)=>{(window.__NEXT_P=window.__NEXT_P||[]).push(["/_not-found/page",function(){return r(3632)}])},6395:(e,t,r)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"HTTPAccessErrorFallback",{enumerable:!0,get:function(){return o}}),r(8229);let l=r(5155);r(2115);let n={error:{fontFamily:'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',height:"100vh",textAlign:"center",display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center"},desc:{display:"inline-block"},h1:{display:"inline-block",margin:"0 20px 0 0",padding:"0 23px 0 0",fontSize:24,fontWeight:500,verticalAlign:"top",lineHeight:"49px"},h2:{fontSize:14,fontWeight:400,lineHeight:"49px",margin:0}};function o(e){let{status:t,message:r}=e;return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)("title",{children:t+": "+r}),(0,l.jsx)("div",{style:n.error,children:(0,l.jsxs)("div",{children:[(0,l.jsx)("style",{dangerouslySetInnerHTML:{__html:"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}),(0,l.jsx)("h1",{className:"next-error-h1",style:n.h1,children:t}),(0,l.jsx)("div",{style:n.desc,children:(0,l.jsx)("h2",{style:n.h2,children:r})})]})})]})}("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)}},e=>{var t=t=>e(e.s=t);e.O(0,[441,684,358],()=>t(3868)),_N_E=e.O()}]);

View file

@ -1 +0,0 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[177],{2705:(e,t,r)=>{Promise.resolve().then(r.bind(r,7780)),Promise.resolve().then(r.t.bind(r,9840,23)),Promise.resolve().then(r.t.bind(r,9324,23))},7780:(e,t,r)=>{"use strict";r.d(t,{ThemeProvider:()=>b});var n=r(5155),s=r(2115),a=(e,t,r,n,s,a,l,o)=>{let c=document.documentElement,i=["light","dark"];function m(t){var r;(Array.isArray(e)?e:[e]).forEach(e=>{let r="class"===e,n=r&&a?s.map(e=>a[e]||e):s;r?(c.classList.remove(...n),c.classList.add(a&&a[t]?a[t]:t)):c.setAttribute(e,t)}),r=t,o&&i.includes(r)&&(c.style.colorScheme=r)}if(n)m(n);else try{let e=localStorage.getItem(t)||r,n=l&&"system"===e?window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light":e;m(n)}catch(e){}},l=["light","dark"],o="(prefers-color-scheme: dark)",c=s.createContext(void 0),i=e=>s.useContext(c)?s.createElement(s.Fragment,null,e.children):s.createElement(d,{...e}),m=["light","dark"],d=e=>{let{forcedTheme:t,disableTransitionOnChange:r=!1,enableSystem:n=!0,enableColorScheme:a=!0,storageKey:i="theme",themes:d=m,defaultTheme:b=n?"system":"light",attribute:p="data-theme",value:v,children:g,nonce:E,scriptProps:S}=e,[k,w]=s.useState(()=>h(i,b)),[C,T]=s.useState(()=>"system"===k?f():k),_=v?Object.values(v):d,L=s.useCallback(e=>{let t=e;if(!t)return;"system"===e&&n&&(t=f());let s=v?v[t]:t,o=r?y(E):null,c=document.documentElement,i=e=>{"class"===e?(c.classList.remove(..._),s&&c.classList.add(s)):e.startsWith("data-")&&(s?c.setAttribute(e,s):c.removeAttribute(e))};if(Array.isArray(p)?p.forEach(i):i(p),a){let e=l.includes(b)?b:null,r=l.includes(t)?t:e;c.style.colorScheme=r}null==o||o()},[E]),A=s.useCallback(e=>{let t="function"==typeof e?e(k):e;w(t);try{localStorage.setItem(i,t)}catch(e){}},[k]),P=s.useCallback(e=>{T(f(e)),"system"===k&&n&&!t&&L("system")},[k,t]);s.useEffect(()=>{let e=window.matchMedia(o);return e.addListener(P),P(e),()=>e.removeListener(P)},[P]),s.useEffect(()=>{let e=e=>{e.key===i&&(e.newValue?w(e.newValue):A(b))};return window.addEventListener("storage",e),()=>window.removeEventListener("storage",e)},[A]),s.useEffect(()=>{L(null!=t?t:k)},[t,k]);let N=s.useMemo(()=>({theme:k,setTheme:A,forcedTheme:t,resolvedTheme:"system"===k?C:k,themes:n?[...d,"system"]:d,systemTheme:n?C:void 0}),[k,A,t,C,n,d]);return s.createElement(c.Provider,{value:N},s.createElement(u,{forcedTheme:t,storageKey:i,attribute:p,enableSystem:n,enableColorScheme:a,defaultTheme:b,value:v,themes:d,nonce:E,scriptProps:S}),g)},u=s.memo(e=>{let{forcedTheme:t,storageKey:r,attribute:n,enableSystem:l,enableColorScheme:o,defaultTheme:c,value:i,themes:m,nonce:d,scriptProps:u}=e,h=JSON.stringify([n,r,c,t,m,i,l,o]).slice(1,-1);return s.createElement("script",{...u,suppressHydrationWarning:!0,nonce:"",dangerouslySetInnerHTML:{__html:"(".concat(a.toString(),")(").concat(h,")")}})}),h=(e,t)=>{let r;try{r=localStorage.getItem(e)||void 0}catch(e){}return r||t},y=e=>{let t=document.createElement("style");return e&&t.setAttribute("nonce",e),t.appendChild(document.createTextNode("*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}")),document.head.appendChild(t),()=>{window.getComputedStyle(document.body),setTimeout(()=>{document.head.removeChild(t)},1)}},f=e=>(e||(e=window.matchMedia(o)),e.matches?"dark":"light");function b(e){let{children:t,...r}=e;return(0,n.jsx)(i,{...r,children:t})}},9324:()=>{},9840:e=>{e.exports={style:{fontFamily:"'Inter', 'Inter Fallback'",fontStyle:"normal"},className:"__className_e8ce0c"}}},e=>{var t=t=>e(e.s=t);e.O(0,[385,441,684,358],()=>t(2705)),_N_E=e.O()}]);

View file

@ -1 +0,0 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[177],{3067:(e,t,r)=>{Promise.resolve().then(r.bind(r,7780)),Promise.resolve().then(r.t.bind(r,9840,23)),Promise.resolve().then(r.t.bind(r,9324,23))},7780:(e,t,r)=>{"use strict";r.d(t,{ThemeProvider:()=>b});var n=r(5155),s=r(2115),a=(e,t,r,n,s,a,l,o)=>{let c=document.documentElement,i=["light","dark"];function m(t){var r;(Array.isArray(e)?e:[e]).forEach(e=>{let r="class"===e,n=r&&a?s.map(e=>a[e]||e):s;r?(c.classList.remove(...n),c.classList.add(a&&a[t]?a[t]:t)):c.setAttribute(e,t)}),r=t,o&&i.includes(r)&&(c.style.colorScheme=r)}if(n)m(n);else try{let e=localStorage.getItem(t)||r,n=l&&"system"===e?window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light":e;m(n)}catch(e){}},l=["light","dark"],o="(prefers-color-scheme: dark)",c=s.createContext(void 0),i=e=>s.useContext(c)?s.createElement(s.Fragment,null,e.children):s.createElement(d,{...e}),m=["light","dark"],d=e=>{let{forcedTheme:t,disableTransitionOnChange:r=!1,enableSystem:n=!0,enableColorScheme:a=!0,storageKey:i="theme",themes:d=m,defaultTheme:b=n?"system":"light",attribute:p="data-theme",value:v,children:g,nonce:E,scriptProps:S}=e,[k,w]=s.useState(()=>h(i,b)),[C,T]=s.useState(()=>"system"===k?f():k),_=v?Object.values(v):d,L=s.useCallback(e=>{let t=e;if(!t)return;"system"===e&&n&&(t=f());let s=v?v[t]:t,o=r?y(E):null,c=document.documentElement,i=e=>{"class"===e?(c.classList.remove(..._),s&&c.classList.add(s)):e.startsWith("data-")&&(s?c.setAttribute(e,s):c.removeAttribute(e))};if(Array.isArray(p)?p.forEach(i):i(p),a){let e=l.includes(b)?b:null,r=l.includes(t)?t:e;c.style.colorScheme=r}null==o||o()},[E]),A=s.useCallback(e=>{let t="function"==typeof e?e(k):e;w(t);try{localStorage.setItem(i,t)}catch(e){}},[k]),P=s.useCallback(e=>{T(f(e)),"system"===k&&n&&!t&&L("system")},[k,t]);s.useEffect(()=>{let e=window.matchMedia(o);return e.addListener(P),P(e),()=>e.removeListener(P)},[P]),s.useEffect(()=>{let e=e=>{e.key===i&&(e.newValue?w(e.newValue):A(b))};return window.addEventListener("storage",e),()=>window.removeEventListener("storage",e)},[A]),s.useEffect(()=>{L(null!=t?t:k)},[t,k]);let N=s.useMemo(()=>({theme:k,setTheme:A,forcedTheme:t,resolvedTheme:"system"===k?C:k,themes:n?[...d,"system"]:d,systemTheme:n?C:void 0}),[k,A,t,C,n,d]);return s.createElement(c.Provider,{value:N},s.createElement(u,{forcedTheme:t,storageKey:i,attribute:p,enableSystem:n,enableColorScheme:a,defaultTheme:b,value:v,themes:d,nonce:E,scriptProps:S}),g)},u=s.memo(e=>{let{forcedTheme:t,storageKey:r,attribute:n,enableSystem:l,enableColorScheme:o,defaultTheme:c,value:i,themes:m,nonce:d,scriptProps:u}=e,h=JSON.stringify([n,r,c,t,m,i,l,o]).slice(1,-1);return s.createElement("script",{...u,suppressHydrationWarning:!0,nonce:"",dangerouslySetInnerHTML:{__html:"(".concat(a.toString(),")(").concat(h,")")}})}),h=(e,t)=>{let r;try{r=localStorage.getItem(e)||void 0}catch(e){}return r||t},y=e=>{let t=document.createElement("style");return e&&t.setAttribute("nonce",e),t.appendChild(document.createTextNode("*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}")),document.head.appendChild(t),()=>{window.getComputedStyle(document.body),setTimeout(()=>{document.head.removeChild(t)},1)}},f=e=>(e||(e=window.matchMedia(o)),e.matches?"dark":"light");function b(e){let{children:t,...r}=e;return(0,n.jsx)(i,{...r,children:t})}},9324:()=>{},9840:e=>{e.exports={style:{fontFamily:"'Inter', 'Inter Fallback'",fontStyle:"normal"},className:"__className_e8ce0c"}}},e=>{var t=t=>e(e.s=t);e.O(0,[385,441,684,358],()=>t(3067)),_N_E=e.O()}]);

View file

@ -0,0 +1 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[177],{1290:()=>{},5039:(e,a,s)=>{Promise.resolve().then(s.t.bind(s,8480,23)),Promise.resolve().then(s.t.bind(s,5680,23)),Promise.resolve().then(s.t.bind(s,1290,23))},5680:e=>{e.exports={style:{fontFamily:"'Geist Mono', 'Geist Mono Fallback'",fontStyle:"normal"},className:"__className_9a8899",variable:"__variable_9a8899"}},8480:e=>{e.exports={style:{fontFamily:"'Geist', 'Geist Fallback'",fontStyle:"normal"},className:"__className_5cfdac",variable:"__variable_5cfdac"}}},e=>{e.O(0,[587,441,255,358],()=>e(e.s=5039)),_N_E=e.O()}]);

View file

@ -1 +0,0 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[974],{3302:(e,r,t)=>{"use strict";t.r(r),t.d(r,{default:()=>h});var s=t(5155),n=t(2115),i=t(4624),a=t(2085),o=t(2596),l=t(9688);let d=(0,a.F)("inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",{variants:{variant:{default:"bg-primary text-primary-foreground hover:bg-primary/90",destructive:"bg-destructive text-destructive-foreground hover:bg-destructive/90",outline:"border border-input bg-background hover:bg-accent hover:text-accent-foreground",secondary:"bg-secondary text-secondary-foreground hover:bg-secondary/80",ghost:"hover:bg-accent hover:text-accent-foreground",link:"text-primary underline-offset-4 hover:underline"},size:{default:"h-10 px-4 py-2",sm:"h-9 rounded-md px-3",lg:"h-11 rounded-md px-8",icon:"h-10 w-10"}},defaultVariants:{variant:"default",size:"default"}}),c=n.forwardRef((e,r)=>{let{className:t,variant:n,size:a,asChild:c=!1,...u}=e,g=c?i.DX:"button";return(0,s.jsx)(g,{className:function(){for(var e=arguments.length,r=Array(e),t=0;t<e;t++)r[t]=arguments[t];return(0,l.QP)((0,o.$)(r))}(d({variant:n,size:a,className:t})),ref:r,...u})});c.displayName="Button";var u=t(9099),g=t(1788),f=t(1415);function h(){return(0,s.jsx)("div",{className:"min-h-screen bg-background text-foreground flex items-center justify-center p-4",children:(0,s.jsxs)("div",{className:"max-w-2xl mx-auto text-center space-y-8",children:[(0,s.jsx)(f.zW,{triggerOnce:!0,children:(0,s.jsx)("h1",{className:"text-4xl md:text-6xl font-bold tracking-tight",children:"ArtistGrid Sheets"})}),(0,s.jsx)(f.zW,{delay:200,triggerOnce:!0,children:(0,s.jsxs)("p",{className:"text-lg md:text-xl text-muted-foreground leading-relaxed",children:["We pull from"," ",(0,s.jsx)("a",{href:"https://docs.google.com/spreadsheets/d/1zoOIaNbBvfuL3sS3824acpqGxOdSZSIHM8-nI9C-Vfc/htmlview",className:"underline text-white",target:"_blank",rel:"noopener noreferrer",children:"TrackerHub"})," ","and parse it into a CSV file. Still a work in progress."]})}),(0,s.jsx)(f.zW,{delay:400,triggerOnce:!0,children:(0,s.jsxs)("div",{className:"flex flex-col sm:flex-row gap-4 justify-center items-center",children:[(0,s.jsx)("a",{href:"https://github.com/ArtistGrid/Sheets",target:"_blank",rel:"noopener noreferrer",children:(0,s.jsxs)(c,{size:"lg",className:"w-full sm:w-auto",children:[(0,s.jsx)(u.A,{className:"mr-2 h-5 w-5"}),"View on GitHub"]})}),(0,s.jsxs)(c,{variant:"outline",size:"lg",className:"w-full sm:w-auto bg-transparent",children:[(0,s.jsx)(g.A,{className:"mr-2 h-5 w-5"}),(0,s.jsx)("a",{href:"https://sheets.artistgrid.cx/artists.csv",target:"_blank",rel:"noopener noreferrer",children:" Download CSV"})]})]})})]})})}},3368:(e,r,t)=>{Promise.resolve().then(t.bind(t,3302))}},e=>{var r=r=>e(e.s=r);e.O(0,[369,441,684,358],()=>r(3368)),_N_E=e.O()}]);

View file

@ -1 +0,0 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[974],{4441:()=>{}},_=>{var e=e=>_(_.s=e);_.O(0,[441,684,358],()=>e(4441)),_N_E=_.O()}]);

View file

@ -0,0 +1 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[974],{5942:(e,r,t)=>{Promise.resolve().then(t.bind(t,6937))},6937:(e,r,t)=>{"use strict";t.r(r),t.d(r,{default:()=>v});var s=t(5155);t(2115);var i=t(6673),a=t(3101),n=t(2821),o=t(5889);let d=(0,a.F)("inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",{variants:{variant:{default:"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",destructive:"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",outline:"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",secondary:"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",ghost:"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",link:"text-primary underline-offset-4 hover:underline"},size:{default:"h-9 px-4 py-2 has-[>svg]:px-3",sm:"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",lg:"h-10 rounded-md px-6 has-[>svg]:px-4",icon:"size-9"}},defaultVariants:{variant:"default",size:"default"}});function l(e){let{className:r,variant:t,size:a,asChild:l=!1,...c}=e,u=l?i.DX:"button";return(0,s.jsx)(u,{"data-slot":"button",className:function(){for(var e=arguments.length,r=Array(e),t=0;t<e;t++)r[t]=arguments[t];return(0,o.QP)((0,n.$)(r))}(d({variant:t,size:a,className:r})),...c})}var c=t(4684),u=t(7676),h=t(9715),g=t(4722);let b=[{name:"View on GitHub",href:"https://github.com/ArtistGrid/Sheets",icon:c.A,isExternal:!0},{name:"Download CSV",href:"https://sheets.artistgrid.cx/artists.csv",icon:u.A,downloadName:"artists.csv"},{name:"View HTML",href:"https://sheets.artistgrid.cx/artists.html",icon:h.A,isExternal:!0},{name:"Download XLSX",href:"https://sheets.artistgrid.cx/artists.xlsx",icon:g.A,downloadName:"ArtistGrid.xlsx"}];function v(){return(0,s.jsx)("div",{className:"min-h-screen bg-black text-white flex items-center justify-center p-4 sm:p-6",children:(0,s.jsxs)("div",{className:"w-full max-w-lg text-center bg-neutral-950 border border-neutral-800 rounded-2xl p-8 sm:p-12 shadow-2xl shadow-black/30 animate-in fade-in-0 zoom-in-95 duration-500",children:[(0,s.jsx)("h1",{className:"text-3xl sm:text-4xl font-bold bg-gradient-to-b from-neutral-50 to-neutral-400 bg-clip-text text-transparent mb-4",children:"ArtistGrid Sheets"}),(0,s.jsx)("p",{className:"text-neutral-400 mb-10 max-w-sm mx-auto",children:"We pull from TrackerHub and parse it into a CSV file. Still a work in progress."}),(0,s.jsx)("div",{className:"grid grid-cols-1 sm:grid-cols-2 gap-4",children:b.map(e=>(0,s.jsx)(l,{asChild:!0,className:"bg-white text-black hover:bg-neutral-200 font-semibold rounded-lg h-14 text-base transition-all duration-300 ease-out hover:-translate-y-1 hover:shadow-[0_0_30px_rgba(255,255,255,0.3)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-black focus-visible:ring-white",children:(0,s.jsxs)("a",{href:e.href,...e.isExternal&&{target:"_blank",rel:"noopener noreferrer"},...e.downloadName&&{download:e.downloadName},children:[(0,s.jsx)(e.icon,{className:"w-5 h-5 mr-2.5","aria-hidden":"true"}),e.name]})},e.name))})]})})}}},e=>{e.O(0,[78,441,255,358],()=>e(e.s=5942)),_N_E=e.O()}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[358],{8259:(e,s,n)=>{Promise.resolve().then(n.t.bind(n,7150,23)),Promise.resolve().then(n.t.bind(n,1959,23)),Promise.resolve().then(n.t.bind(n,7989,23)),Promise.resolve().then(n.t.bind(n,3886,23)),Promise.resolve().then(n.t.bind(n,9766,23)),Promise.resolve().then(n.t.bind(n,5278,23)),Promise.resolve().then(n.t.bind(n,8924,23)),Promise.resolve().then(n.t.bind(n,4431,23)),Promise.resolve().then(n.bind(n,622))},9393:()=>{}},e=>{var s=s=>e(e.s=s);e.O(0,[441,255],()=>(s(1666),s(8259))),_N_E=e.O()}]);

View file

@ -1 +0,0 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[358],{2585:(e,s,n)=>{Promise.resolve().then(n.t.bind(n,894,23)),Promise.resolve().then(n.t.bind(n,4970,23)),Promise.resolve().then(n.t.bind(n,6614,23)),Promise.resolve().then(n.t.bind(n,6975,23)),Promise.resolve().then(n.t.bind(n,7555,23)),Promise.resolve().then(n.t.bind(n,4911,23)),Promise.resolve().then(n.t.bind(n,9665,23)),Promise.resolve().then(n.t.bind(n,1295,23))}},e=>{var s=s=>e(e.s=s);e.O(0,[441,684],()=>(s(5415),s(2585))),_N_E=e.O()}]);

View file

@ -1 +0,0 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[358],{9941:(e,s,n)=>{Promise.resolve().then(n.t.bind(n,894,23)),Promise.resolve().then(n.t.bind(n,4970,23)),Promise.resolve().then(n.t.bind(n,6614,23)),Promise.resolve().then(n.t.bind(n,6975,23)),Promise.resolve().then(n.t.bind(n,7555,23)),Promise.resolve().then(n.t.bind(n,4911,23)),Promise.resolve().then(n.t.bind(n,9665,23)),Promise.resolve().then(n.t.bind(n,1295,23))}},e=>{var s=s=>e(e.s=s);e.O(0,[441,684],()=>(s(5415),s(9941))),_N_E=e.O()}]);

View file

@ -0,0 +1 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[139,636],{326:(e,t,n)=>{(window.__NEXT_P=window.__NEXT_P||[]).push(["/_app",function(){return n(5139)}])},5139:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"default",{enumerable:!0,get:function(){return s}});let u=n(4252),l=n(7876),a=u._(n(4232)),o=n(1033);async function r(e){let{Component:t,ctx:n}=e;return{pageProps:await (0,o.loadGetInitialProps)(t,n)}}class s extends a.default.Component{render(){let{Component:e,pageProps:t}=this.props;return(0,l.jsx)(e,{...t})}}s.origGetInitialProps=r,s.getInitialProps=r,("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)}},e=>{var t=t=>e(e.s=t);e.O(0,[593,792],()=>(t(326),t(6763))),_N_E=e.O()}]);

View file

@ -1 +0,0 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[472,636],{326:(e,t,n)=>{(window.__NEXT_P=window.__NEXT_P||[]).push(["/_app",function(){return n(472)}])},472:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"default",{enumerable:!0,get:function(){return s}});let u=n(4252),l=n(7876),a=u._(n(4232)),o=n(2746);async function r(e){let{Component:t,ctx:n}=e;return{pageProps:await (0,o.loadGetInitialProps)(t,n)}}class s extends a.default.Component{render(){let{Component:e,pageProps:t}=this.props;return(0,l.jsx)(e,{...t})}}s.origGetInitialProps=r,s.getInitialProps=r,("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)}},e=>{var t=t=>e(e.s=t);e.O(0,[593,792],()=>(t(326),t(4294))),_N_E=e.O()}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
(()=>{"use strict";var e={},t={};function r(o){var n=t[o];if(void 0!==n)return n.exports;var a=t[o]={exports:{}},i=!0;try{e[o](a,a.exports,r),i=!1}finally{i&&delete t[o]}return a.exports}r.m=e,(()=>{var e=[];r.O=(t,o,n,a)=>{if(o){a=a||0;for(var i=e.length;i>0&&e[i-1][2]>a;i--)e[i]=e[i-1];e[i]=[o,n,a];return}for(var u=1/0,i=0;i<e.length;i++){for(var[o,n,a]=e[i],c=!0,l=0;l<o.length;l++)(!1&a||u>=a)&&Object.keys(r.O).every(e=>r.O[e](o[l]))?o.splice(l--,1):(c=!1,a<u&&(u=a));if(c){e.splice(i--,1);var s=n();void 0!==s&&(t=s)}}return t}})(),(()=>{var e,t=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__;r.t=function(o,n){if(1&n&&(o=this(o)),8&n||"object"==typeof o&&o&&(4&n&&o.__esModule||16&n&&"function"==typeof o.then))return o;var a=Object.create(null);r.r(a);var i={};e=e||[null,t({}),t([]),t(t)];for(var u=2&n&&o;"object"==typeof u&&!~e.indexOf(u);u=t(u))Object.getOwnPropertyNames(u).forEach(e=>i[e]=()=>o[e]);return i.default=()=>o,r.d(a,i),a}})(),r.d=(e,t)=>{for(var o in t)r.o(t,o)&&!r.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:t[o]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((t,o)=>(r.f[o](e,t),t),[])),r.u=e=>"static/chunks/"+e+"."+({341:"2903e54d3da731c1",472:"a3826d29d6854395"})[e]+".js",r.miniCssF=e=>{},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||Function("return this")()}catch(e){if("object"==typeof window)return window}}(),r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={},t="_N_E:";r.l=(o,n,a,i)=>{if(e[o]){e[o].push(n);return}if(void 0!==a)for(var u,c,l=document.getElementsByTagName("script"),s=0;s<l.length;s++){var d=l[s];if(d.getAttribute("src")==o||d.getAttribute("data-webpack")==t+a){u=d;break}}u||(c=!0,(u=document.createElement("script")).charset="utf-8",u.timeout=120,r.nc&&u.setAttribute("nonce",r.nc),u.setAttribute("data-webpack",t+a),u.src=r.tu(o)),e[o]=[n];var f=(t,r)=>{u.onerror=u.onload=null,clearTimeout(p);var n=e[o];if(delete e[o],u.parentNode&&u.parentNode.removeChild(u),n&&n.forEach(e=>e(r)),t)return t(r)},p=setTimeout(f.bind(null,void 0,{type:"timeout",target:u}),12e4);u.onerror=f.bind(null,u.onerror),u.onload=f.bind(null,u.onload),c&&document.head.appendChild(u)}})(),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e;r.tt=()=>(void 0===e&&(e={createScriptURL:e=>e},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("nextjs#bundler",e))),e)})(),r.tu=e=>r.tt().createScriptURL(e),r.p="/_next/",(()=>{var e={68:0,385:0};r.f.j=(t,o)=>{var n=r.o(e,t)?e[t]:void 0;if(0!==n){if(n)o.push(n[2]);else if(/^(385|68)$/.test(t))e[t]=0;else{var a=new Promise((r,o)=>n=e[t]=[r,o]);o.push(n[2]=a);var i=r.p+r.u(t),u=Error();r.l(i,o=>{if(r.o(e,t)&&(0!==(n=e[t])&&(e[t]=void 0),n)){var a=o&&("load"===o.type?"missing":o.type),i=o&&o.target&&o.target.src;u.message="Loading chunk "+t+" failed.\n("+a+": "+i+")",u.name="ChunkLoadError",u.type=a,u.request=i,n[1](u)}},"chunk-"+t,t)}}},r.O.j=t=>0===e[t];var t=(t,o)=>{var n,a,[i,u,c]=o,l=0;if(i.some(t=>0!==e[t])){for(n in u)r.o(u,n)&&(r.m[n]=u[n]);if(c)var s=c(r)}for(t&&t(o);l<i.length;l++)a=i[l],r.o(e,a)&&e[a]&&e[a][0](),e[a]=0;return r.O(s)},o=self.webpackChunk_N_E=self.webpackChunk_N_E||[];o.forEach(t.bind(null,0)),o.push=t.bind(null,o.push.bind(o))})()})();

View file

@ -0,0 +1 @@
(()=>{"use strict";var e={},t={};function r(o){var n=t[o];if(void 0!==n)return n.exports;var a=t[o]={exports:{}},i=!0;try{e[o](a,a.exports,r),i=!1}finally{i&&delete t[o]}return a.exports}r.m=e,(()=>{var e=[];r.O=(t,o,n,a)=>{if(o){a=a||0;for(var i=e.length;i>0&&e[i-1][2]>a;i--)e[i]=e[i-1];e[i]=[o,n,a];return}for(var u=1/0,i=0;i<e.length;i++){for(var[o,n,a]=e[i],l=!0,c=0;c<o.length;c++)(!1&a||u>=a)&&Object.keys(r.O).every(e=>r.O[e](o[c]))?o.splice(c--,1):(l=!1,a<u&&(u=a));if(l){e.splice(i--,1);var s=n();void 0!==s&&(t=s)}}return t}})(),(()=>{var e,t=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__;r.t=function(o,n){if(1&n&&(o=this(o)),8&n||"object"==typeof o&&o&&(4&n&&o.__esModule||16&n&&"function"==typeof o.then))return o;var a=Object.create(null);r.r(a);var i={};e=e||[null,t({}),t([]),t(t)];for(var u=2&n&&o;"object"==typeof u&&!~e.indexOf(u);u=t(u))Object.getOwnPropertyNames(u).forEach(e=>i[e]=()=>o[e]);return i.default=()=>o,r.d(a,i),a}})(),r.d=(e,t)=>{for(var o in t)r.o(t,o)&&!r.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:t[o]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((t,o)=>(r.f[o](e,t),t),[])),r.u=e=>"static/chunks/"+e+"."+({139:"7a5a8e93a21948c1",646:"f342b7cffc01feb0"})[e]+".js",r.miniCssF=e=>{},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||Function("return this")()}catch(e){if("object"==typeof window)return window}}(),r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={},t="_N_E:";r.l=(o,n,a,i)=>{if(e[o])return void e[o].push(n);if(void 0!==a)for(var u,l,c=document.getElementsByTagName("script"),s=0;s<c.length;s++){var f=c[s];if(f.getAttribute("src")==o||f.getAttribute("data-webpack")==t+a){u=f;break}}u||(l=!0,(u=document.createElement("script")).charset="utf-8",u.timeout=120,r.nc&&u.setAttribute("nonce",r.nc),u.setAttribute("data-webpack",t+a),u.src=r.tu(o)),e[o]=[n];var d=(t,r)=>{u.onerror=u.onload=null,clearTimeout(p);var n=e[o];if(delete e[o],u.parentNode&&u.parentNode.removeChild(u),n&&n.forEach(e=>e(r)),t)return t(r)},p=setTimeout(d.bind(null,void 0,{type:"timeout",target:u}),12e4);u.onerror=d.bind(null,u.onerror),u.onload=d.bind(null,u.onload),l&&document.head.appendChild(u)}})(),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e;r.tt=()=>(void 0===e&&(e={createScriptURL:e=>e},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("nextjs#bundler",e))),e)})(),r.tu=e=>r.tt().createScriptURL(e),r.p="/_next/",(()=>{var e={68:0,587:0};r.f.j=(t,o)=>{var n=r.o(e,t)?e[t]:void 0;if(0!==n)if(n)o.push(n[2]);else if(/^(587|68)$/.test(t))e[t]=0;else{var a=new Promise((r,o)=>n=e[t]=[r,o]);o.push(n[2]=a);var i=r.p+r.u(t),u=Error();r.l(i,o=>{if(r.o(e,t)&&(0!==(n=e[t])&&(e[t]=void 0),n)){var a=o&&("load"===o.type?"missing":o.type),i=o&&o.target&&o.target.src;u.message="Loading chunk "+t+" failed.\n("+a+": "+i+")",u.name="ChunkLoadError",u.type=a,u.request=i,n[1](u)}},"chunk-"+t,t)}},r.O.j=t=>0===e[t];var t=(t,o)=>{var n,a,[i,u,l]=o,c=0;if(i.some(t=>0!==e[t])){for(n in u)r.o(u,n)&&(r.m[n]=u[n]);if(l)var s=l(r)}for(t&&t(o);c<i.length;c++)a=i[c],r.o(e,a)&&e[a]&&e[a][0](),e[a]=0;return r.O(s)},o=self.webpackChunk_N_E=self.webpackChunk_N_E||[];o.forEach(t.bind(null,0)),o.push=t.bind(null,o.push.bind(o))})()})();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
self.__BUILD_MANIFEST=function(e,r,t){return{__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},__routerFilterStatic:{numItems:2,errorRate:1e-4,numBits:39,numHashes:14,bitArray:[0,1,1,0,r,e,e,r,r,e,e,r,e,e,e,r,r,e,e,e,e,r,e,r,r,r,r,e,e,e,r,e,r,e,r,e,e,e,r]},__routerFilterDynamic:{numItems:r,errorRate:1e-4,numBits:r,numHashes:null,bitArray:[]},"/_error":["static/chunks/pages/_error-71d2b6a7b832d02a.js"],sortedPages:["/_app","/_error"]}}(1,0,1e-4),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();

View file

@ -1 +0,0 @@
self.__SSG_MANIFEST=new Set([]);self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()

View file

@ -0,0 +1 @@
self.__BUILD_MANIFEST=function(e,r,t){return{__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},__routerFilterStatic:{numItems:3,errorRate:1e-4,numBits:58,numHashes:14,bitArray:[1,1,0,e,0,e,e,r,e,e,r,e,e,e,r,e,r,r,e,r,r,r,e,r,r,r,r,r,e,r,e,e,e,e,r,e,e,r,e,e,e,r,e,r,e,r,r,e,e,e,r,r,e,e,e,r,e,e]},__routerFilterDynamic:{numItems:r,errorRate:1e-4,numBits:r,numHashes:null,bitArray:[]},"/_error":["static/chunks/pages/_error-013f4188946cdd04.js"],sortedPages:["/_app","/_error"]}}(1,0,1e-4),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View file

@ -1,18 +1,20 @@
1:"$Sreact.fragment" 1:"$Sreact.fragment"
2:I[7780,["177","static/chunks/app/layout-546c94f4a8071dd0.js"],"ThemeProvider"] 2:I[9766,[],""]
3:I[7555,[],""] 3:I[8924,[],""]
4:I[1295,[],""] 4:I[1959,[],"ClientPageRoot"]
5:I[894,[],"ClientPageRoot"] 5:I[6937,["78","static/chunks/78-578bf7339c7a46f2.js","974","static/chunks/app/page-d0a5f652f053f84b.js"],"default"]
6:I[3302,["369","static/chunks/369-033af3d47e75a2e9.js","974","static/chunks/app/page-369ac552c0f033d3.js"],"default"] 8:I[4431,[],"OutletBoundary"]
9:I[9665,[],"OutletBoundary"] a:I[5278,[],"AsyncMetadataOutlet"]
c:I[9665,[],"ViewportBoundary"] c:I[4431,[],"ViewportBoundary"]
e:I[9665,[],"MetadataBoundary"] e:I[4431,[],"MetadataBoundary"]
10:I[6614,[],""] f:"$Sreact.suspense"
:HL["/_next/static/css/5c4bd492bdff6129.css","style"] 11:I[7150,[],""]
0:{"P":null,"b":"GswdzUCnMYMZOzXXFJekf","p":"","c":["",""],"i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/5c4bd492bdff6129.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__className_e8ce0c","children":["$","$L2",null,{"attribute":"class","defaultTheme":"dark","enableSystem":true,"disableTransitionOnChange":true,"children":["$","$L3",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]]}],{"children":["__PAGE__",["$","$1","c",{"children":[["$","$L5",null,{"Component":"$6","searchParams":{},"params":{},"promises":["$@7","$@8"]}],"$undefined",null,["$","$L9",null,{"children":["$La","$Lb",null]}]]}],{},null,false]},null,false],["$","$1","h",{"children":[null,["$","$1","NLGqWe3sR1VBX3v8KTt8Y",{"children":[["$","$Lc",null,{"children":"$Ld"}],null]}],["$","$Le",null,{"children":"$Lf"}]]}],false]],"m":"$undefined","G":["$10","$undefined"],"s":false,"S":true} :HL["/_next/static/css/323a36643e3c1db1.css","style"]
7:{} 0:{"P":null,"b":"luWZjwc8VZvb8hGtFuZa2","p":"","c":["",""],"i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/323a36643e3c1db1.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"__variable_5cfdac __variable_9a8899 antialiased","children":["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]]}],{"children":["__PAGE__",["$","$1","c",{"children":[["$","$L4",null,{"Component":"$5","searchParams":{},"params":{},"promises":["$@6","$@7"]}],null,["$","$L8",null,{"children":["$L9",["$","$La",null,{"promise":"$@b"}]]}]]}],{},null,false]},null,false],["$","$1","h",{"children":[null,[["$","$Lc",null,{"children":"$Ld"}],null],["$","$Le",null,{"children":["$","div",null,{"hidden":true,"children":["$","$f",null,{"fallback":null,"children":"$L10"}]}]}]]}],false]],"m":"$undefined","G":["$11",[]],"s":false,"S":true}
8:{} 6:{}
7:"$0:f:0:1:2:children:1:props:children:0:props:params"
d:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]] d:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
a:null 9:null
b:null 12:I[622,[],"IconMark"]
f:[["$","title","0",{"children":"ArtistGrid Sheets"}],["$","link","1",{"rel":"icon","href":"./favicon.png"}]] b:{"metadata":[["$","title","0",{"children":"ArtistGrid Sheets"}],["$","meta","1",{"name":"description","content":"We pull from TrackerHub and parse it into a CSV file. Still a work in progress."}],["$","link","2",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","link","3",{"rel":"icon","href":"/favicon.png"}],["$","$L12","4",{}]],"error":null,"digest":"$undefined"}
10:"$b:metadata"

View file

@ -1,76 +1,97 @@
# update_loop.py
import json import json
import logging
import os import os
import time import time
from datetime import datetime from datetime import datetime, timezone
from downloader import download_zip_and_extract_html, download_xlsx
from parser import generate_csv
from diff import read_csv_to_dict, detect_changes
from archive import archive_all_urls from archive import archive_all_urls
from config import CSV_FILENAME, HTML_FILENAME, XLSX_FILENAME
from diff import detect_changes, read_csv_to_dict
from downloader import download_xlsx, download_zip_and_extract_html
from notify import send_discord_message from notify import send_discord_message
from parser import generate_csv
from utils import hash_file from utils import hash_file
logger = logging.getLogger(__name__)
last_html_hash = None last_html_hash = None
last_csv_data = {} last_csv_data = {}
INFO_PATH = "info/status.json" INFO_PATH = os.path.join("info", "status.json")
UPDATE_INTERVAL_SECONDS = 600
def write_info(html_hash, csv_hash, xlsx_hash):
def write_info(html_hash: str, csv_hash: str, xlsx_hash: str, is_archived: bool):
os.makedirs("info", exist_ok=True) os.makedirs("info", exist_ok=True)
info = { now_iso = datetime.now(timezone.utc).isoformat()
"last_updated": datetime.utcnow().isoformat() + "Z",
"files": { try:
"Artists.html": { with open(INFO_PATH, "r") as f:
"hash": html_hash, info = json.load(f)
"last_archived": datetime.utcnow().isoformat() + "Z" except (FileNotFoundError, json.JSONDecodeError):
}, info = {"files": {HTML_FILENAME: {}}}
"artists.csv": {
"hash": csv_hash info["last_updated"] = now_iso
}, info["files"][HTML_FILENAME]["hash"] = html_hash
"artists.xlsx": { if is_archived:
"hash": xlsx_hash info["files"][HTML_FILENAME]["last_archived"] = now_iso
}
} info["files"][CSV_FILENAME] = {"hash": csv_hash}
} info["files"][XLSX_FILENAME] = {"hash": xlsx_hash}
with open(INFO_PATH, "w") as f: with open(INFO_PATH, "w") as f:
json.dump(info, f, indent=2) json.dump(info, f, indent=2)
def update_loop(): def update_loop():
global last_html_hash, last_csv_data global last_html_hash, last_csv_data
while True: while True:
logger.info("--- Starting update cycle ---")
try: try:
download_zip_and_extract_html() download_zip_and_extract_html()
download_xlsx() download_xlsx()
generate_csv() generate_csv()
html_hash = hash_file("Artists.html") if not all(
csv_hash = hash_file("artists.csv") os.path.exists(f) for f in [HTML_FILENAME, CSV_FILENAME, XLSX_FILENAME]
xlsx_hash = hash_file("artists.xlsx") ):
logger.warning(
"One or more files are missing after download/parse. Skipping this cycle."
)
time.sleep(UPDATE_INTERVAL_SECONDS)
continue
current_data = read_csv_to_dict("artists.csv") html_hash = hash_file(HTML_FILENAME)
csv_hash = hash_file(CSV_FILENAME)
xlsx_hash = hash_file(XLSX_FILENAME)
current_csv_data = read_csv_to_dict(CSV_FILENAME)
archived_this_cycle = False
if last_html_hash is None: if last_html_hash is None:
print(" Initial HTML hash stored.") logger.info("First run: storing initial file hashes.")
elif html_hash != last_html_hash: elif html_hash != last_html_hash:
print("🔔 Artists.html has changed! Archiving URLs...") logger.info("🔔 Artists.html has changed! Checking for data differences.")
changes = detect_changes(last_csv_data, current_csv_data)
changes = detect_changes(last_csv_data, current_data)
if changes: if changes:
message = "**CSV Update Detected:**\n" + "\n".join(changes) message = "**Tracker Update Detected:**\n" + "\n".join(changes)
send_discord_message(message) send_discord_message(message)
archive_all_urls()
archived_this_cycle = True
else: else:
print(" No detectable content changes found in CSV.") logger.info(" HTML hash changed, but no data differences found.")
archive_all_urls()
else: else:
print(" Artists.html unchanged. No archiving needed.") logger.info(" Artists.html is unchanged.")
write_info(html_hash, csv_hash, xlsx_hash)
write_info(html_hash, csv_hash, xlsx_hash, is_archived=archived_this_cycle)
last_html_hash = html_hash last_html_hash = html_hash
last_csv_data = current_data last_csv_data = current_csv_data
logger.info("--- Update cycle finished ---")
except Exception as e: except Exception as e:
print(f"⚠️ Error updating files: {e}") logger.critical(
f"An unexpected error occurred in the update loop: {e}", exc_info=True
)
time.sleep(600) logger.info(f"Sleeping for {UPDATE_INTERVAL_SECONDS} seconds...")
time.sleep(UPDATE_INTERVAL_SECONDS)

View file

@ -1,14 +1,22 @@
import re, hashlib # utils.py
import hashlib
import re
def clean_artist_name(text):
return re.sub(r'[⭐🤖🎭\u2B50\uFE0F]', '', text).strip()
def force_star_flag(starred=True): def clean_artist_name(text: str) -> str:
return re.sub(r"[⭐🤖🎭\u2B50\uFE0F]", "", text).strip()
def force_star_flag(starred: bool = True) -> str:
return "Yes" if starred else "No" return "Yes" if starred else "No"
def hash_file(filename):
def hash_file(filename: str, block_size: int = 65536) -> str:
hasher = hashlib.sha256() hasher = hashlib.sha256()
with open(filename, "rb") as f: try:
buf = f.read() with open(filename, "rb") as f:
hasher.update(buf) for block in iter(lambda: f.read(block_size), b""):
hasher.update(block)
except FileNotFoundError:
return "file_not_found"
return hasher.hexdigest() return hasher.hexdigest()