DiscogsDataProcessorCLI 1.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
discogs/main.py ADDED
@@ -0,0 +1,170 @@
1
+ # discogs/main.py
2
+
3
+ import typer
4
+ from discogs.selector import show_welcome, display_status_table, select_indices
5
+ from discogs.scraper import get_latest_files
6
+ from discogs.downloader import download_files_threaded
7
+ from discogs.extractor import extract_gz_files
8
+ from discogs.converter import convert_xml_to_csv
9
+ from discogs.config import get_download_dir
10
+ from discogs.utils import open_folder
11
+ from pathlib import Path
12
+ from rich.console import Console
13
+ import time
14
+
15
+ # Initialize CLI app with help text
16
+ app = typer.Typer(
17
+ help="šŸ“¦ Discogs CLI - Download, extract, and convert Discogs data dumps.",
18
+ invoke_without_command=True
19
+ )
20
+
21
+ console = Console()
22
+
23
+ @app.command(help="One-click pipeline: Fetch latest files, download, extract, and convert to CSV.")
24
+ def run():
25
+ """
26
+ Full automated pipeline: shows welcome screen, fetches files,
27
+ lets user choose which ones to download, then downloads, extracts,
28
+ and converts them to CSV.
29
+ """
30
+ show_welcome()
31
+ download_dir = get_download_dir()
32
+
33
+ typer.echo("\U0001F50D Fetching available Discogs files...")
34
+ df = get_latest_files()
35
+
36
+ if df.empty:
37
+ typer.echo("No data found.")
38
+ raise typer.Exit()
39
+
40
+ display_status_table(df, download_dir)
41
+ indices = select_indices(df)
42
+
43
+ if not indices:
44
+ typer.echo("No selection made.")
45
+ raise typer.Exit()
46
+
47
+ start = time.time()
48
+
49
+ downloaded = download_files_threaded(df, indices, download_dir)
50
+ extracted = extract_gz_files(downloaded)
51
+
52
+ for xml_file in extracted:
53
+ content_type = xml_file.stem.split("_")[-1]
54
+ convert_xml_to_csv(xml_file, content_type)
55
+
56
+ duration = time.time() - start
57
+ typer.secho(f"\nāœ… Done in {duration:.1f} seconds!", fg="green")
58
+ open_folder(download_dir)
59
+
60
+ @app.command()
61
+ @app.command()
62
+ def download():
63
+ """
64
+ Download selected Discogs data files only (no extract or convert).
65
+ """
66
+ download_dir = get_download_dir()
67
+ typer.echo("\U0001F50D Fetching available Discogs files...")
68
+ df = get_latest_files()
69
+
70
+ if df.empty:
71
+ typer.echo("No data found.")
72
+ raise typer.Exit()
73
+
74
+ display_status_table(df, download_dir)
75
+ indices = select_indices(df)
76
+ if not indices:
77
+ typer.echo("No files selected.")
78
+ raise typer.Exit()
79
+
80
+ download_files_threaded(df, indices, download_dir)
81
+
82
+ open_folder(download_dir)
83
+
84
+ @app.command()
85
+ def convert():
86
+ """Convert extracted XML files to CSV (interactive mode)."""
87
+ from discogs.converter import convert_interactively
88
+ convert_interactively()
89
+
90
+ @app.command()
91
+ def extract():
92
+ """Extract downloaded .gz files (interactive mode)."""
93
+ from discogs.extractor import extract_interactively
94
+ extract_interactively()
95
+
96
+ @app.command("delete")
97
+ def delete(all: bool = typer.Option(False, "--all", help="Delete all downloaded, extracted and converted files.")):
98
+ """
99
+ Deletes selected or all downloaded, extracted, and converted files.
100
+ """
101
+ download_dir = get_download_dir()
102
+ df = get_latest_files()
103
+
104
+ if df.empty:
105
+ console.print("[red]No files found.[/red]")
106
+ raise typer.Exit()
107
+
108
+ display_status_table(df, download_dir)
109
+
110
+ # If --all is passed, select all files
111
+ selected = list(range(len(df))) if all else select_indices(df, allow_all=True)
112
+
113
+ if not selected:
114
+ console.print("[yellow]No files selected.[/yellow]")
115
+ raise typer.Exit()
116
+
117
+ for i in selected:
118
+ row = df.iloc[i]
119
+ year_month = row["month"]
120
+ filename = Path(row["url"]).name
121
+ data_dir = download_dir / "Datasets" / year_month
122
+
123
+ gz_file = data_dir / filename
124
+ xml_file = gz_file.with_suffix("")
125
+ csv_file = xml_file.with_suffix(".csv")
126
+
127
+ for file in [gz_file, xml_file, csv_file]:
128
+ if file.exists():
129
+ try:
130
+ file.unlink()
131
+ console.print(f"[green]āœ” Deleted:[/] {file.name}")
132
+ except Exception as e:
133
+ console.print(f"[red]āœ— Failed to delete {file.name}:[/] {e}")
134
+ else:
135
+ console.print(f"[dim]• Not found:[/] {file.name}")
136
+
137
+ @app.command()
138
+ def show():
139
+ """
140
+ Displays the list of available Discogs dump files.
141
+ """
142
+ from discogs.selector import show_welcome
143
+ from discogs.scraper import get_latest_files
144
+ from discogs.config import get_download_dir
145
+
146
+ show_welcome()
147
+ df = get_latest_files()
148
+ display_status_table(df, get_download_dir())
149
+
150
+ @app.command()
151
+ def config():
152
+ """Launches the download folder configuration prompt."""
153
+ from discogs.config import set_download_dir
154
+ set_download_dir()
155
+
156
+ @app.callback()
157
+ def main(ctx: typer.Context):
158
+ """
159
+ If no subcommand is provided, defaults to running the `run` pipeline.
160
+ """
161
+ if ctx.invoked_subcommand is None:
162
+ run()
163
+
164
+ # Automatically run the `run` pipeline if no arguments are provided
165
+ if __name__ == "__main__":
166
+ import sys
167
+ if len(sys.argv) == 1:
168
+ app(prog_name="discogs", args=["run"])
169
+ else:
170
+ app()
discogs/scraper.py ADDED
@@ -0,0 +1,125 @@
1
+ # discogs/scraper.py
2
+
3
+ import re
4
+ import requests
5
+ import pandas as pd
6
+ import xml.etree.ElementTree as ET
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from discogs.config import get_download_dir
10
+
11
+ # Base URL of the Discogs S3 bucket
12
+ S3_BASE_URL = "https://discogs-data-dumps.s3.us-west-2.amazonaws.com/"
13
+ S3_PREFIX = "data/" # Prefix for data folders inside the bucket
14
+
15
+ def list_directories() -> list[str]:
16
+ """
17
+ Lists available yearly folders on the Discogs S3 bucket.
18
+ Example: data/2024/
19
+ """
20
+ url = f"{S3_BASE_URL}?prefix={S3_PREFIX}&delimiter=/"
21
+ r = requests.get(url)
22
+ r.raise_for_status()
23
+
24
+ ns = "{http://s3.amazonaws.com/doc/2006-03-01/}"
25
+ root = ET.fromstring(r.text)
26
+
27
+ dirs = []
28
+ for cp in root.findall(ns + 'CommonPrefixes'):
29
+ p = cp.find(ns + 'Prefix').text
30
+ if re.match(r"data/\d{4}/", p): # Match folders like "data/2023/"
31
+ dirs.append(p)
32
+
33
+ return sorted(dirs)
34
+
35
+ def list_files(directory_prefix: str) -> pd.DataFrame:
36
+ """
37
+ Lists files in the specified S3 folder and extracts metadata like size,
38
+ last modified date, type (artists, labels, etc.), and generates their URLs.
39
+ """
40
+ url = f"{S3_BASE_URL}?prefix={directory_prefix}"
41
+ r = requests.get(url)
42
+ r.raise_for_status()
43
+
44
+ ns = "{http://s3.amazonaws.com/doc/2006-03-01/}"
45
+ root = ET.fromstring(r.text)
46
+
47
+ data = []
48
+ for content in root.findall(ns + 'Contents'):
49
+ key = content.find(ns + 'Key').text
50
+ size = int(content.find(ns + 'Size').text)
51
+ last_modified = content.find(ns + 'LastModified').text
52
+
53
+ # Determine content type from filename
54
+ ctype = "unknown"
55
+ lname = key.lower()
56
+ if "artist" in lname:
57
+ ctype = "artists"
58
+ elif "label" in lname:
59
+ ctype = "labels"
60
+ elif "master" in lname:
61
+ ctype = "masters"
62
+ elif "release" in lname:
63
+ ctype = "releases"
64
+
65
+ # Filter only usable .gz files
66
+ if ctype != "unknown" and key.endswith(".gz"):
67
+ month = get_month_from_key(key)
68
+ filename = Path(key).name
69
+ data.append({
70
+ "key": key,
71
+ "size_bytes": size,
72
+ "last_modified": last_modified,
73
+ "month": month,
74
+ "content": ctype,
75
+ "filename": filename,
76
+ "url": S3_BASE_URL + key,
77
+ })
78
+
79
+ df = pd.DataFrame(data)
80
+
81
+ # Add download/extracted/converted status columns
82
+ download_dir = get_download_dir()
83
+ df["downloaded"] = df["filename"].apply(
84
+ lambda fn: (download_dir / "Datasets" / get_month_from_key(fn) / fn).exists()
85
+ )
86
+ df["extracted"] = df["filename"].apply(
87
+ lambda fn: (download_dir / "Datasets" / get_month_from_key(fn) / fn.replace(".gz", "")).exists()
88
+ )
89
+ df["converted"] = df["filename"].apply(
90
+ lambda fn: (download_dir / "Datasets" / get_month_from_key(fn) / fn.replace(".gz", ".csv")).exists()
91
+ )
92
+
93
+ return df
94
+
95
+ def get_month_from_key(key: str) -> str:
96
+ """
97
+ Extracts the year and month from the Discogs filename.
98
+ Example: discogs_20240101_artist.gz → 2024-01
99
+ """
100
+ match = re.search(r"discogs_(\d{6})\d{2}", key)
101
+ if match:
102
+ try:
103
+ return datetime.strptime(match.group(1), "%Y%m").strftime("%Y-%m")
104
+ except Exception:
105
+ return ""
106
+ return ""
107
+
108
+ def get_latest_files() -> pd.DataFrame:
109
+ """
110
+ Fetches and returns a DataFrame with files from the most recent available S3 folder.
111
+ """
112
+ dirs = list_directories()
113
+ if not dirs:
114
+ return pd.DataFrame()
115
+
116
+ latest_dir = dirs[-1]
117
+ df = list_files(latest_dir)
118
+
119
+ if df.empty:
120
+ return df
121
+
122
+ # Parse dates and sort by month and type
123
+ df["last_modified"] = pd.to_datetime(df["last_modified"])
124
+ df = df.sort_values(by=["month", "content"], ascending=[False, True]).reset_index(drop=True)
125
+ return df
discogs/selector.py ADDED
@@ -0,0 +1,197 @@
1
+ # discogs/selector.py
2
+
3
+ from rich.prompt import Prompt
4
+ from typing import List
5
+ import pandas as pd
6
+ from rich.panel import Panel
7
+ from rich.markdown import Markdown
8
+ from rich.table import Table
9
+ from rich.console import Console
10
+ from discogs.utils import human_readable_size
11
+ from pathlib import Path
12
+
13
+ console = Console()
14
+
15
+ def display_table(df: pd.DataFrame) -> None:
16
+ """
17
+ Displays a Rich-formatted table of available Discogs files.
18
+ Shows basic info: index, month, content type, file size, and URL.
19
+ """
20
+ table = Table(title="Available Discogs Files", show_lines=True)
21
+
22
+ table.add_column("No", style="cyan", justify="right")
23
+ table.add_column("Month", style="magenta")
24
+ table.add_column("Type", style="green")
25
+ table.add_column("Size (MB)", justify="right")
26
+ table.add_column("URL", style="dim", overflow="fold")
27
+
28
+ for i, row in df.iterrows():
29
+ size_mb = f"{row['size_bytes'] / (1024 ** 2):.2f}"
30
+ table.add_row(
31
+ str(i + 1),
32
+ row["month"],
33
+ row["content"],
34
+ size_mb,
35
+ row["url"]
36
+ )
37
+
38
+ console.print(table)
39
+
40
+ def select_indices(df: pd.DataFrame, allow_all: bool = False) -> List[int]:
41
+ """
42
+ Prompts user to select files by number (comma-separated list or 'all' if allowed).
43
+ Returns a list of selected row indices.
44
+ """
45
+ while True:
46
+ selection = Prompt.ask(
47
+ "[bold green]Select file(s) by number (comma-separated)[/]",
48
+ default="1"
49
+ )
50
+
51
+ if allow_all and selection.strip().lower() == "all":
52
+ return list(range(len(df)))
53
+
54
+ try:
55
+ selected = [int(x.strip()) - 1 for x in selection.split(",")]
56
+ if all(0 <= i < len(df) for i in selected):
57
+ return selected
58
+ else:
59
+ raise ValueError
60
+ except ValueError:
61
+ console.print("[red]Invalid selection. Try again.[/red]")
62
+
63
+ def select_files(df: pd.DataFrame) -> List[int]:
64
+ """
65
+ Allows user to select files using basic printed list.
66
+ Returns selected row indices.
67
+ """
68
+ if df.empty:
69
+ console.print("[red]No files to select.[/red]")
70
+ return []
71
+
72
+ for i, row in df.iterrows():
73
+ size_mb = f"{row['size_bytes'] / (1024 ** 2):.2f} MB"
74
+ console.print(f"[{i + 1}] {row['month']} | {row['content']} | {size_mb}")
75
+
76
+ while True:
77
+ selection = Prompt.ask("Select file(s) by number (comma-separated)", default="1")
78
+ try:
79
+ indices = [int(x.strip()) - 1 for x in selection.split(",")]
80
+ if all(0 <= i < len(df) for i in indices):
81
+ return indices
82
+ except Exception:
83
+ pass
84
+
85
+ console.print("[red]Invalid selection. Try again.[/red]")
86
+
87
+ def display_status_table(df, download_dir: Path):
88
+ """
89
+ Displays the full download/extract/convert status of all files in a table.
90
+ Includes āœ”/āœ— markers for each status column.
91
+ """
92
+ table = Table(title="Available Discogs Files", show_lines=True)
93
+ table.add_column("No", justify="right", style="cyan", no_wrap=True)
94
+ table.add_column("Month", style="magenta")
95
+ table.add_column("Type", style="yellow")
96
+ table.add_column("Size", justify="right")
97
+ table.add_column("Downloaded", justify="center")
98
+ table.add_column("Extracted", justify="center")
99
+ table.add_column("Converted", justify="center")
100
+
101
+ for idx, row in df.iterrows():
102
+ filename = Path(row["url"]).name
103
+ year_month = row["month"]
104
+ data_dir = download_dir / "Datasets" / year_month
105
+ gz_path = data_dir / filename
106
+ xml_path = gz_path.with_suffix("")
107
+ csv_path = xml_path.with_suffix(".csv")
108
+
109
+ is_downloaded = gz_path.exists()
110
+ is_extracted = xml_path.exists()
111
+ is_converted = csv_path.exists()
112
+
113
+ check = lambda b: "[green]āœ”[/green]" if b else "[red]āœ—[/red]"
114
+
115
+ table.add_row(
116
+ str(idx + 1),
117
+ row["month"],
118
+ row["content"],
119
+ human_readable_size(row["size_bytes"]),
120
+ check(is_downloaded),
121
+ check(is_extracted),
122
+ check(is_converted),
123
+ )
124
+
125
+ console.print(table)
126
+
127
+ def show_welcome():
128
+ """
129
+ Displays the ASCII welcome screen with a summary of available commands and features.
130
+ """
131
+ ascii_logo = r"""
132
+ ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—
133
+ ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā•”ā•ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•”ā•ā•ā•ā•ā• ā–ˆā–ˆā•”ā•ā•ā•ā•ā•
134
+ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—
135
+ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā•šā•ā•ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā•šā•ā•ā•ā•ā–ˆā–ˆā•‘
136
+ ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘
137
+ ā•šā•ā•ā•ā•ā•ā• ā•šā•ā•ā•šā•ā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•ā•ā•ā•
138
+
139
+ ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā•—
140
+ ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā•šā•ā•ā–ˆā–ˆā•”ā•ā•ā•ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—
141
+ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘
142
+ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•‘
143
+ ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘
144
+ ā•šā•ā•ā•ā•ā•ā• ā•šā•ā• ā•šā•ā• ā•šā•ā• ā•šā•ā• ā•šā•ā•
145
+
146
+ ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—
147
+ ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•”ā•ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā•”ā•ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—
148
+ ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•
149
+ ā–ˆā–ˆā•”ā•ā•ā•ā• ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•”ā•ā•ā• ā•šā•ā•ā•ā•ā–ˆā–ˆā•‘ā•šā•ā•ā•ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—
150
+ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘
151
+ ā•šā•ā• ā•šā•ā• ā•šā•ā• ā•šā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•ā•ā•ā•šā•ā•ā•ā•ā•ā•ā•ā•šā•ā•ā•ā•ā•ā•ā•ā•šā•ā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•ā•ā• ā•šā•ā• ā•šā•ā•
152
+ """
153
+
154
+ console.print(Panel.fit(
155
+ ascii_logo,
156
+ title="Discogs Data Processor CLI",
157
+ subtitle="by ofurkancoban",
158
+ style="bold cyan"
159
+ ))
160
+
161
+ md = Markdown("""
162
+ Welcome to the **Discogs CLI**!
163
+
164
+ This tool allows you to:
165
+ - 🧠 Scrape the latest data dump list from Discogs
166
+ - ā¬‡ļø Download selected files
167
+ - šŸ“¦ Extract `.gz` files
168
+ - āœ‚ļø Chunk large XML into smaller files
169
+ - šŸ“„ Convert everything into tidy CSV
170
+ - šŸ—‘ Delete downloaded/extracted/converted files
171
+ - āš™ļø Configure your download folder
172
+
173
+ ---
174
+
175
+ **Available Commands:**
176
+
177
+ - `python -m discogs.main run` — Full auto mode (download → extract → convert)
178
+ - `python -m discogs.main show` — Display available Discogs files
179
+ - `python -m discogs.main download` — Download selected files
180
+ - `python -m discogs.main extract` — Extract previously downloaded `.gz` files
181
+ - `python -m discogs.main convert` — Convert extracted `.xml` files to `.csv`
182
+ - `python -m discogs.main delete` — Delete files by selection (or `--all`)
183
+ - `python -m discogs.main config` — Set or change your download folder
184
+
185
+ ---
186
+
187
+ **Connect with me:**
188
+
189
+ - 🌐 GitHub: [github.com/ofurkancoban](https://github.com/ofurkancoban)
190
+ - šŸ’¼ LinkedIn: [linkedin.com/in/ofurkancoban](https://linkedin.com/in/ofurkancoban)
191
+ - šŸ“Š Kaggle: [kaggle.com/ofurkancoban](https://www.kaggle.com/ofurkancoban)
192
+
193
+ ---
194
+
195
+ """)
196
+
197
+ console.print(md)
discogs/utils.py ADDED
@@ -0,0 +1,96 @@
1
+ # discogs/utils.py
2
+
3
+ import json
4
+ from rich.prompt import Prompt
5
+ from rich.console import Console
6
+ import subprocess
7
+ import platform
8
+ from pathlib import Path
9
+
10
+ console = Console()
11
+
12
+ CONFIG_PATH = Path.home() / "Downloads" / "Discogs" / ".discogs_config.json"
13
+ DEFAULT_DOWNLOAD_PATH = Path.home() / "Downloads" / "Discogs"
14
+
15
+ def load_config() -> dict:
16
+ """
17
+ Loads the config JSON from disk.
18
+ Returns default config if file doesn't exist or can't be read.
19
+ """
20
+ if CONFIG_PATH.exists():
21
+ try:
22
+ with CONFIG_PATH.open("r") as f:
23
+ return json.load(f)
24
+ except Exception as e:
25
+ console.print(f"[red]⚠ Error reading config:[/] {e}")
26
+ return {"download_dir": str(DEFAULT_DOWNLOAD_PATH)}
27
+
28
+ def save_config(config: dict):
29
+ """
30
+ Saves the provided config dictionary as JSON to disk.
31
+ """
32
+ try:
33
+ with CONFIG_PATH.open("w") as f:
34
+ json.dump(config, f, indent=2)
35
+ console.print(f"[green]āœ” Config saved:[/] {CONFIG_PATH}")
36
+ except Exception as e:
37
+ console.print(f"[red]⚠ Failed to save config:[/] {e}")
38
+
39
+ def get_download_dir() -> Path:
40
+ """
41
+ Returns the current download directory from config.
42
+ Falls back to default if not configured.
43
+ """
44
+ config = load_config()
45
+ return Path(config.get("download_dir", str(DEFAULT_DOWNLOAD_PATH)))
46
+
47
+ def open_folder(path: Path):
48
+ """
49
+ Opens the given folder path using the system's default file explorer.
50
+ Supports macOS, Windows, and Linux.
51
+ """
52
+ try:
53
+ if platform.system() == "Darwin": # macOS
54
+ subprocess.run(["open", str(path)])
55
+ elif platform.system() == "Windows":
56
+ subprocess.run(["explorer", str(path)])
57
+ else: # Linux (assumes xdg-open is available)
58
+ subprocess.run(["xdg-open", str(path)])
59
+ except Exception as e:
60
+ print(f"Error opening folder: {e}")
61
+
62
+ def human_readable_size(size_bytes: int) -> str:
63
+ """
64
+ Converts a file size in bytes into a human-readable string (KB, MB, GB, etc).
65
+ """
66
+ if size_bytes == 0:
67
+ return "0 B"
68
+
69
+ size_name = ("B", "KB", "MB", "GB", "TB")
70
+ i = 0
71
+ double_size = float(size_bytes)
72
+ while double_size >= 1024 and i < len(size_name) - 1:
73
+ double_size /= 1024
74
+ i += 1
75
+ return f"{double_size:.2f} {size_name[i]}"
76
+
77
+ def set_download_dir():
78
+ """
79
+ Prompts the user to enter a new download folder and updates the config.
80
+ Creates the folder if it doesn't exist.
81
+ """
82
+ current = get_download_dir()
83
+ new_path = Prompt.ask("Download folder", default=str(current)).strip()
84
+ path = Path(new_path).expanduser()
85
+
86
+ if not path.exists():
87
+ try:
88
+ path.mkdir(parents=True)
89
+ console.print(f"[green]āœ” Created directory:[/] {path}")
90
+ except Exception as e:
91
+ console.print(f"[red]Failed to create directory:[/] {e}")
92
+ return
93
+
94
+ config = load_config()
95
+ config["download_dir"] = str(path)
96
+ save_config(config)