Python script to download complete discography from tracks.json data file. Creates organized album directories with cover images and MP3 tracks. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
179 lines
6.5 KiB
Python
Executable File
179 lines
6.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Dopo Goto Music Downloader
|
|
Downloads music tracks and cover images from tracks.json
|
|
"""
|
|
|
|
####################################################################################################
|
|
# Copyright (C) 2026 by WallyHackenslacker wallyhackenslacker@noreply.git.hackenslacker.space #
|
|
# #
|
|
# Permission to use, copy, modify, and/or distribute this software for any purpose with or without #
|
|
# fee is hereby granted. #
|
|
# #
|
|
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS #
|
|
# SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE #
|
|
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES #
|
|
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, #
|
|
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE #
|
|
# OF THIS SOFTWARE. #
|
|
####################################################################################################
|
|
|
|
import os
|
|
import re
|
|
import json
|
|
import time
|
|
import random
|
|
import requests
|
|
from urllib.parse import urlparse, unquote
|
|
|
|
|
|
def parse_js_object(content: str) -> dict:
|
|
"""Convert JavaScript object notation to valid JSON and parse it."""
|
|
# Quote unquoted keys - keys appear after { , [ or at line start with whitespace
|
|
# Match word characters that are followed by : but not already quoted
|
|
content = re.sub(r'(^|[{\[,])\s*(\w+)(\s*:)', r'\1"\2"\3', content, flags=re.MULTILINE)
|
|
|
|
# Remove trailing commas before ] or } (not valid in JSON)
|
|
content = re.sub(r',(\s*[\]}])', r'\1', content)
|
|
|
|
# Remove the outer braces and extract content
|
|
content = content.strip()
|
|
if content.startswith('{'):
|
|
content = content[1:]
|
|
if content.endswith('}'):
|
|
content = content[:-1]
|
|
|
|
# The structure has albums: {first_album}, {second_album}, ...
|
|
# We need to wrap the albums in an array
|
|
# Find "albums": and wrap everything after it in an array
|
|
albums_match = re.search(r'"albums"\s*:\s*\{', content)
|
|
if albums_match:
|
|
# Find the start position after "albums":
|
|
start_pos = albums_match.start()
|
|
prefix = content[:start_pos]
|
|
rest = content[start_pos:]
|
|
|
|
# Replace "albums": { with "albums": [{
|
|
rest = re.sub(r'"albums"\s*:\s*\{', '"albums": [{', rest, count=1)
|
|
|
|
# Add closing bracket at the end
|
|
rest = rest.rstrip().rstrip(',')
|
|
rest += ']'
|
|
|
|
content = prefix + rest
|
|
|
|
# Wrap in braces to make valid JSON object
|
|
content = '{' + content + '}'
|
|
|
|
# Parse the JSON
|
|
return json.loads(content)
|
|
|
|
|
|
def download_file(url: str, filepath: str) -> bool:
|
|
"""Download a file from URL to the specified filepath."""
|
|
try:
|
|
print(f" Downloading: {os.path.basename(filepath)}")
|
|
response = requests.get(url, stream=True, timeout=60)
|
|
response.raise_for_status()
|
|
|
|
with open(filepath, 'wb') as f:
|
|
for chunk in response.iter_content(chunk_size=8192):
|
|
f.write(chunk)
|
|
|
|
print(f" Downloaded: {os.path.basename(filepath)}")
|
|
return True
|
|
except requests.RequestException as e:
|
|
print(f" Error downloading {url}: {e}")
|
|
return False
|
|
|
|
|
|
def get_filename_from_url(url: str) -> str:
|
|
"""Extract and decode filename from URL."""
|
|
parsed = urlparse(url)
|
|
path = unquote(parsed.path)
|
|
return os.path.basename(path)
|
|
|
|
|
|
def get_extension_from_url(url: str) -> str:
|
|
"""Extract file extension from URL."""
|
|
filename = get_filename_from_url(url)
|
|
_, ext = os.path.splitext(filename)
|
|
return ext
|
|
|
|
|
|
def main():
|
|
# Read the tracks.json file
|
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
tracks_file = os.path.join(script_dir, 'tracks.json')
|
|
|
|
print(f"Reading {tracks_file}...")
|
|
with open(tracks_file, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
# Parse the JavaScript object notation
|
|
print("Parsing track data...")
|
|
data = parse_js_object(content)
|
|
albums = data.get('albums', [])
|
|
|
|
print(f"Found {len(albums)} albums\n")
|
|
|
|
# Process each album
|
|
for album_idx, album in enumerate(albums, 1):
|
|
album_name = album.get('name', f'Unknown Album {album_idx}')
|
|
cover_url = album.get('cover', '')
|
|
tracks = album.get('tracks', [])
|
|
|
|
# Create album directory
|
|
album_dir = os.path.join(script_dir, f"Dopo Goto - {album_name}")
|
|
os.makedirs(album_dir, exist_ok=True)
|
|
|
|
print(f"[{album_idx}/{len(albums)}] Processing: {album_name}")
|
|
print(f" Directory: {album_dir}")
|
|
|
|
# Download cover image
|
|
if cover_url:
|
|
cover_ext = get_extension_from_url(cover_url)
|
|
cover_filepath = os.path.join(album_dir, f"cover{cover_ext}")
|
|
|
|
if os.path.exists(cover_filepath):
|
|
print(f" Cover already exists, skipping")
|
|
else:
|
|
download_file(cover_url, cover_filepath)
|
|
# Random delay after download
|
|
delay = random.uniform(0.5, 3.0)
|
|
print(f" Waiting {delay:.1f}s...")
|
|
time.sleep(delay)
|
|
|
|
# Download tracks
|
|
for track_idx, track in enumerate(tracks, 1):
|
|
track_name = track.get('name', f'Track {track_idx}')
|
|
track_url = track.get('file', '')
|
|
|
|
if not track_url:
|
|
print(f" Skipping track {track_idx}: no URL")
|
|
continue
|
|
|
|
# Get filename from URL
|
|
track_filename = get_filename_from_url(track_url)
|
|
track_filepath = os.path.join(album_dir, track_filename)
|
|
|
|
if os.path.exists(track_filepath):
|
|
print(f" [{track_idx}/{len(tracks)}] Already exists: {track_filename}")
|
|
continue
|
|
|
|
print(f" [{track_idx}/{len(tracks)}] {track_name}")
|
|
download_file(track_url, track_filepath)
|
|
|
|
# Random delay between downloads
|
|
delay = random.uniform(0.5, 3.0)
|
|
print(f" Waiting {delay:.1f}s...")
|
|
time.sleep(delay)
|
|
|
|
print(f" Album complete!\n")
|
|
|
|
print("All downloads complete!")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|