diff --git a/.gitignore b/.gitignore index 4b0854c..eb586cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -logs/*.log +data/ .env docker-compose.yml +__pycache__ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 014a9b1..83a9783 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,5 +10,11 @@ COPY . . # Install any needed packages specified in requirements.txt RUN pip install --no-cache-dir -r requirements.txt -# Use an entrypoint script -ENTRYPOINT ["python", "./sptnr.py"] +# Expose port 5000 for the Flask app +EXPOSE 5000 + +# Set the entrypoint to Python +ENTRYPOINT ["python"] + +# Leave CMD empty +CMD [] diff --git a/VERSION b/VERSION index 867e524..359a5b9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.0 \ No newline at end of file +2.0.0 \ No newline at end of file diff --git a/docker-compose.yml.example b/docker-compose.yml.example index 0240927..8dc574c 100644 --- a/docker-compose.yml.example +++ b/docker-compose.yml.example @@ -15,5 +15,9 @@ services: - NAV_PASS=your_navidrome_password - SPOTIFY_CLIENT_ID=your_spotify_client_id - SPOTIFY_CLIENT_SECRET=your_spotify_client_secret + - WEB_API_KEY=changeme volumes: - - ./logs:/usr/src/app/logs + - ./data:/usr/src/app/data + ports: + - "3333:3333" + command: web_server.py diff --git a/requirements.txt b/requirements.txt index 6f2a7ab..0a9b519 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,7 @@ requests==2.31.0 python-dotenv==1.0.0 colorama==0.4.6 -tqdm==4.66.1 \ No newline at end of file +tqdm==4.66.1 +gunicorn==20.1.0 +Flask==2.1.2 +Werkzeug==2.0.3 \ No newline at end of file diff --git a/sptnr.py b/sptnr.py index 38db694..7306037 100644 --- a/sptnr.py +++ b/sptnr.py @@ -41,7 +41,7 @@ BOLD = Style.BRIGHT RESET = Style.RESET_ALL # Setup logs -LOG_DIR = "logs" +LOG_DIR = "data/logs" if not os.path.exists(LOG_DIR): os.makedirs(LOG_DIR) @@ -103,6 +103,9 @@ TOTAL_TRACKS = 0 FOUND_AND_UPDATED = 0 NOT_FOUND = 0 UNMATCHED_TRACKS = [] +PROCESSED_ALBUMS_FILE = "data/processed_albums.txt" + +processed_albums = set() # Parse arguments description_text = "process command-line flags for sync" @@ -147,6 +150,13 @@ parser.add_argument( "-v", "--version", action="version", version=f"%(prog)s {__version__}" ) +parser.add_argument( + "-f", + "--force", + action="store_true", + help="force processing of all albums, even if they were processed previously", +) + args = parser.parse_args() @@ -295,6 +305,13 @@ def process_track(track_id, artist_name, album, track_name): def process_album(album_id): + if not args.force: + global processed_albums + + if album_id in processed_albums: + logging.info(f" {LIGHT_CYAN}Skipping already processed album{RESET}") + return + nav_url = f"{NAV_BASE_URL}/rest/getAlbum?id={album_id}&u={NAV_USER}&p=enc:{HEX_ENCODED_PASS}&v=1.12.0&c=spotify_sync&f=json" response = requests.get(nav_url).json() @@ -308,6 +325,10 @@ def process_album(album_id): for track in tracks: process_track(*track) + processed_albums.add(album_id) + with open(PROCESSED_ALBUMS_FILE, "a") as file: + file.write(f"{album_id}\n") + def process_artist(artist_id): nav_url = f"{NAV_BASE_URL}/rest/getArtist?id={artist_id}&u={NAV_USER}&p=enc:{HEX_ENCODED_PASS}&v=1.12.0&c=spotify_sync&f=json" @@ -360,6 +381,11 @@ def fetch_data(url): sys.exit(1) +# Load processed albums +if os.path.exists(PROCESSED_ALBUMS_FILE) and not args.force: + with open(PROCESSED_ALBUMS_FILE, "r") as file: + processed_albums = set(file.read().splitlines()) + try: validate_url(NAV_BASE_URL) except ValueError as e: @@ -426,14 +452,26 @@ else: # Display the results logging.info("") -MATCH_PERCENTAGE = (FOUND_AND_UPDATED / TOTAL_TRACKS) * 100 if TOTAL_TRACKS != 0 else 0 -FORMATTED_MATCH_PERCENTAGE = round(MATCH_PERCENTAGE, 2) # Rounding to 2 decimal places + +# Check if TOTAL_TRACKS is zero to avoid division by zero error +if TOTAL_TRACKS > 0: + MATCH_PERCENTAGE = (FOUND_AND_UPDATED / TOTAL_TRACKS) * 100 +else: + MATCH_PERCENTAGE = 0 + +FORMATTED_MATCH_PERCENTAGE = round(MATCH_PERCENTAGE, 2) TOTAL_BLOCKS = 20 color_found = LIGHT_GREEN if FOUND_AND_UPDATED == TOTAL_TRACKS else LIGHT_YELLOW color_found_white = LIGHT_GREEN if FOUND_AND_UPDATED == TOTAL_TRACKS else BOLD color_not_found = LIGHT_GREEN if NOT_FOUND == 0 else LIGHT_RED -blocks_found = "█" * round(FOUND_AND_UPDATED * TOTAL_BLOCKS / TOTAL_TRACKS) + +# Adjust the progress bar calculation +blocks_found = ( + "█" * round(FOUND_AND_UPDATED * TOTAL_BLOCKS / TOTAL_TRACKS) + if TOTAL_TRACKS > 0 + else "" +) blocks_not_found = "█" * (TOTAL_BLOCKS - len(blocks_found)) full_blocks_found = f"{color_found_white}{blocks_found}{RESET}" full_blocks_not_found = f"{color_not_found}{blocks_not_found}{RESET}" @@ -453,7 +491,6 @@ if seconds or not parts: # Show seconds if it's the only value, even if it's 0 formatted_elapsed_time = " ".join(parts) -# logging.info(f"Processing completed in {int(hours):02}:{int(minutes):02}:{int(seconds):02}") logging.info( f"Tracks: {LIGHT_PURPLE}{TOTAL_TRACKS}{RESET} | Found: {color_found}{FOUND_AND_UPDATED}{RESET} |{full_blocks_found}{full_blocks_not_found}| Not Found: {color_not_found}{NOT_FOUND}{RESET} | Match: {color_found}{FORMATTED_MATCH_PERCENTAGE}%{RESET} | Time: {LIGHT_PURPLE}{formatted_elapsed_time}{RESET}" ) diff --git a/web_server.py b/web_server.py new file mode 100644 index 0000000..4b59e77 --- /dev/null +++ b/web_server.py @@ -0,0 +1,86 @@ +from flask import Flask, request, jsonify +import json +import subprocess +import threading +import os +from dotenv import load_dotenv +import datetime + +load_dotenv() + +sptnr_web_server = Flask(__name__) +API_KEY = os.getenv("WEB_API_KEY") +LOG_DIR = "data/logs" + + +def run_script(cmd): + subprocess.run(cmd) + + +def log_post_data(data): + timestamp = datetime.datetime.now().isoformat() + log_filename = os.path.join(LOG_DIR, f"log_{timestamp}.txt") + with open(log_filename, "w") as log_file: + json.dump(data, log_file, indent=4) + + +@sptnr_web_server.route("/process", methods=["GET", "POST"]) +def process_request(): + if request.args.get("api_key") != API_KEY: + return jsonify({"error": "Unauthorized"}), 401 + + cmd = ["python3", "sptnr.py"] + if request.args.get("preview", "") == "true": + cmd.append("--preview") + if request.args.get("force", "") == "true": + cmd.append("--force") + artist_ids = request.args.getlist("artist") + for artist_id in artist_ids: + cmd.extend(["--artist", artist_id]) + album_ids = request.args.getlist("album") + for album_id in album_ids: + cmd.extend(["--album", album_id]) + start = request.args.get("start") + if start: + cmd.extend(["--start", start]) + limit = request.args.get("limit") + if limit: + cmd.extend(["--limit", limit]) + + thread = threading.Thread(target=run_script, args=(cmd,)) + thread.start() + + return jsonify({"message": "Processing started"}) + + +@sptnr_web_server.route("/logs") +def list_logs(): + try: + logs = os.listdir(LOG_DIR) + logs = [log for log in logs if log.endswith(".log")] # Filter log files + + log_links = "".join(f'
  • {log}
  • ' for log in logs) + return f"

    Log Files

    " + except Exception as e: + return f"An error occurred: {e}", 500 + + +@sptnr_web_server.route("/logs/") +def view_log(filename): + try: + full_path = os.path.join(LOG_DIR, filename) + if not os.path.exists(full_path) or not os.path.isfile(full_path): + return "File not found", 404 + + with open(full_path, "r") as file: + content = file.read() + + # Convert content to HTML-friendly format + content = content.replace("\n", "
    ") + return f'

    {filename}

    {content}
    ' + except Exception as e: + return f"An error occurred: {e}", 500 + + +if __name__ == "__main__": + sptnr_web_server.run(debug=False, host="0.0.0.0", port=3333)