feat(misc): added script to generate library for Jellyfin
This commit is contained in:
@@ -0,0 +1,182 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import random
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.StreamHandler(sys.stdout)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_DATASET_URL = "https://raw.githubusercontent.com/sidooms/MovieTweetings/master/latest/movies.dat"
|
||||||
|
DEFAULT_OUTPUT_DIR = "./Jellyfin_Movies"
|
||||||
|
DEFAULT_COUNT = 1000
|
||||||
|
|
||||||
|
def download_dataset(url):
|
||||||
|
logger.info(f"Downloading dataset from {url}...")
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
||||||
|
with urllib.request.urlopen(req) as response:
|
||||||
|
data = response.read().decode('utf-8')
|
||||||
|
logger.info("Dataset downloaded successfully.")
|
||||||
|
return data.splitlines()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to download dataset: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def parse_movies(lines):
|
||||||
|
"""
|
||||||
|
Parse the movies.dat file.
|
||||||
|
Format: IMDbID::Title (Year)::Genres
|
||||||
|
Example: 0000008::Edison Kinetoscopic Record of a Sneeze (1894)::Documentary|Short
|
||||||
|
"""
|
||||||
|
movies = []
|
||||||
|
# Regex to extract Title and Year from "Title (Year)"
|
||||||
|
title_year_pattern = re.compile(r'(.*)\s+\((\d{4})\)$')
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
parts = line.split('::')
|
||||||
|
if len(parts) >= 2:
|
||||||
|
imdb_id_raw = parts[0]
|
||||||
|
title_year_raw = parts[1]
|
||||||
|
|
||||||
|
# Format IMDb ID to ttXXXXXXX
|
||||||
|
if imdb_id_raw.isdigit():
|
||||||
|
imdb_id = f"tt{imdb_id_raw.zfill(7)}"
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
match = title_year_pattern.match(title_year_raw)
|
||||||
|
if match:
|
||||||
|
title = match.group(1).strip()
|
||||||
|
year = match.group(2)
|
||||||
|
|
||||||
|
# Clean title for filesystem (remove invalid characters)
|
||||||
|
safe_title = re.sub(r'[\\/*?:"<>|]', "", title)
|
||||||
|
safe_title = safe_title.strip()
|
||||||
|
|
||||||
|
if safe_title:
|
||||||
|
movies.append({
|
||||||
|
'imdb_id': imdb_id,
|
||||||
|
'title': safe_title,
|
||||||
|
'year': year
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(f"Parsed {len(movies)} valid movies from dataset.")
|
||||||
|
return movies
|
||||||
|
|
||||||
|
def create_dummy_video(filepath):
|
||||||
|
"""Create a minimal valid dummy video file (mp4)."""
|
||||||
|
try:
|
||||||
|
mp4_header = b"\x00\x00\x00\x18ftypmp42\x00\x00\x00\x00mp42isom\x00\x00\x00\x00moov\x00\x00\x00\x08mvhd"
|
||||||
|
with open(filepath, 'wb') as f:
|
||||||
|
f.write(mp4_header)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to create dummy video {filepath}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def generate_library(movies, output_dir, count):
|
||||||
|
"""Generate the folder structure and dummy files."""
|
||||||
|
if not os.path.exists(output_dir):
|
||||||
|
os.makedirs(output_dir)
|
||||||
|
logger.info(f"Created output directory: {output_dir}")
|
||||||
|
|
||||||
|
generated_imdb_ids = set()
|
||||||
|
created_count = 0
|
||||||
|
skipped_count = 0
|
||||||
|
failed_count = 0
|
||||||
|
|
||||||
|
logger.info(f"Starting generation of up to {count} movies...")
|
||||||
|
|
||||||
|
# Shuffle to get diverse movies
|
||||||
|
random.shuffle(movies)
|
||||||
|
|
||||||
|
for movie in movies:
|
||||||
|
if created_count >= count:
|
||||||
|
break
|
||||||
|
|
||||||
|
if movie['imdb_id'] in generated_imdb_ids:
|
||||||
|
skipped_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Jellyfin naming convention: Movie Name (year) [imdbid-tt1234567]
|
||||||
|
folder_name = f"{movie['title']} ({movie['year']}) [imdbid-{movie['imdb_id']}]"
|
||||||
|
folder_path = os.path.join(output_dir, folder_name)
|
||||||
|
|
||||||
|
file_name = f"{folder_name}.mp4"
|
||||||
|
file_path = os.path.join(folder_path, file_name)
|
||||||
|
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
skipped_count += 1
|
||||||
|
generated_imdb_ids.add(movie['imdb_id'])
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(folder_path, exist_ok=True)
|
||||||
|
if create_dummy_video(file_path):
|
||||||
|
created_count += 1
|
||||||
|
generated_imdb_ids.add(movie['imdb_id'])
|
||||||
|
else:
|
||||||
|
failed_count += 1
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing {folder_name}: {e}")
|
||||||
|
failed_count += 1
|
||||||
|
|
||||||
|
logger.info("--- Generation Summary ---")
|
||||||
|
logger.info(f"Target count: {count}")
|
||||||
|
logger.info(f"Successfully created: {created_count}")
|
||||||
|
logger.info(f"Skipped (already exists or duplicate): {skipped_count}")
|
||||||
|
logger.info(f"Failed: {failed_count}")
|
||||||
|
|
||||||
|
return created_count > 0
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Generate a dummy Jellyfin movie library.")
|
||||||
|
parser.add_argument("--output-dir", type=str, default=DEFAULT_OUTPUT_DIR,
|
||||||
|
help=f"Directory to create the library in (default: {DEFAULT_OUTPUT_DIR})")
|
||||||
|
parser.add_argument("--count", type=int, default=DEFAULT_COUNT,
|
||||||
|
help=f"Number of movies to generate (default: {DEFAULT_COUNT})")
|
||||||
|
parser.add_argument("--dataset-url", type=str, default=DEFAULT_DATASET_URL,
|
||||||
|
help="URL to the movies.dat file")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
lines = download_dataset(args.dataset_url)
|
||||||
|
if not lines:
|
||||||
|
logger.error("No dataset lines to process.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
movies = parse_movies(lines)
|
||||||
|
|
||||||
|
if not movies:
|
||||||
|
logger.error("No movies parsed from the dataset.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if len(movies) < args.count:
|
||||||
|
logger.warning(f"Requested {args.count} movies, but only {len(movies)} available.")
|
||||||
|
args.count = len(movies)
|
||||||
|
|
||||||
|
success = generate_library(movies, args.output_dir, args.count)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.info(f"Library generation complete. You can now mount '{os.path.abspath(args.output_dir)}' into Jellyfin.")
|
||||||
|
else:
|
||||||
|
logger.error("Library generation failed.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user