From a78e4b7b78ee2f8a5666788cc8be18a8adf8305e Mon Sep 17 00:00:00 2001 From: blast Date: Sat, 21 Sep 2024 19:25:06 +0200 Subject: [PATCH 01/36] fix songs removed from spotify playlists --- spotisub/core/database/database.py | 21 ++++ spotisub/core/external/classes/classes.py | 8 ++ spotisub/core/subsonic_helper.py | 124 ++++++++++++---------- uwsgi.ini | 1 + 4 files changed, 97 insertions(+), 57 deletions(-) create mode 100644 spotisub/core/external/classes/classes.py diff --git a/spotisub/core/database/database.py b/spotisub/core/database/database.py index 9904d13..09f65df 100644 --- a/spotisub/core/database/database.py +++ b/spotisub/core/database/database.py @@ -151,6 +151,27 @@ def delete_playlist_relation_by_id(self, playlist_id: str): conn.commit() conn.close() +def delete_song_relation(self, playlist_id: str, subsonic_track): + """delete playlist from database""" + + if "id" in subsonic_track: + stmt = None + if "artistId" in subsonic_track: + stmt = delete(self.subsonic_spotify_relation).where( + self.subsonic_spotify_relation.c.subsonic_playlist_id == playlist_id, + self.subsonic_spotify_relation.c.subsonic_song_id == subsonic_track["id"], + self.subsonic_spotify_relation.c.subsonic_artist_id == subsonic_track["artistId"]) + else: + stmt = delete(self.subsonic_spotify_relation).where( + self.subsonic_spotify_relation.c.subsonic_playlist_id == playlist_id, + self.subsonic_spotify_relation.c.subsonic_song_id == subsonic_track["id"]) + + stmt.compile() + with self.db_engine.connect() as conn: + conn.execute(stmt) + conn.commit() + conn.close() + def insert_playlist_relation(self, conn, subsonic_song_id, subsonic_artist_id, subsonic_playlist_id, spotify_song_uuid): diff --git a/spotisub/core/external/classes/classes.py b/spotisub/core/external/classes/classes.py new file mode 100644 index 0000000..0707cba --- /dev/null +++ b/spotisub/core/external/classes/classes.py @@ -0,0 +1,8 @@ +class ComparisonHelper: + def __init__(self, track, artist_spotify, found, excluded, song_ids, track_helper): + self.track = track + self.artist_spotify = artist_spotify + self.found = found + self.excluded = excluded + self.song_ids = song_ids + self.track_helper = track_helper \ No newline at end of file diff --git a/spotisub/core/subsonic_helper.py b/spotisub/core/subsonic_helper.py index a50babd..c1db191 100644 --- a/spotisub/core/subsonic_helper.py +++ b/spotisub/core/subsonic_helper.py @@ -13,6 +13,7 @@ from .external.exceptions.exceptions import SubsonicOfflineException from .database import database from .external import musicbrainz_helper +from .external.classes.classes import ComparisonHelper if os.environ.get(constants.SPOTDL_ENABLED, @@ -155,13 +156,14 @@ def write_playlist(sp, playlist_name, results): "") + playlist_name playlist_id = get_playlist_id_by_name(playlist_name) song_ids = [] + old_song_ids = [] if playlist_id is None: check_pysonic_connection().createPlaylist(name=playlist_name, songIds=[]) logging.info('Creating playlist %s', playlist_name) playlist_id = get_playlist_id_by_name(playlist_name) database.delete_playlist_relation_by_id(dbms, playlist_id) else: - song_ids = get_playlist_songs_ids_by_id(playlist_id) + old_song_ids = get_playlist_songs_ids_by_id(playlist_id) track_helper = [] for track in results['tracks']: @@ -176,19 +178,24 @@ def write_playlist(sp, playlist_name, results): artist_spotify["name"], track['name']) if "name" in track: - (track, - artist_spotify, - found, - excluded, - song_ids, - track_helper) = match_with_subsonic_track( - track, + comparison_helper = ComparisonHelper(track, artist_spotify, - found, excluded, + found, + excluded, song_ids, - track_helper, + track_helper) + comparison_helper = match_with_subsonic_track( + comparison_helper, playlist_id, + old_song_ids, playlist_name) + + track = comparison_helper.track + artist_spotify = comparison_helper.artist_spotify + found = comparison_helper.found + excluded = comparison_helper.excluded + song_ids = comparison_helper.song_ids + track_helper = comparison_helper.track_helper if not excluded: if (os.environ.get(constants.SPOTDL_ENABLED, constants.SPOTDL_ENABLED_DEFAULT_VALUE) == "1" @@ -226,13 +233,16 @@ def write_playlist(sp, playlist_name, results): track['name']) database.insert_song( dbms, playlist_id, None, artist_spotify, track) - - if len(song_ids) > 0: - logging.info('Success! Created playlist %s', playlist_name) - elif len(song_ids) == 0: - if playlist_id is not None: + if playlist_id is not None: + + if len(song_ids) > 0: + check_pysonic_connection().createPlaylist( + playlistId=playlist_id, songIds=song_ids) + logging.info('Success! Created playlist %s', playlist_name) + elif len(song_ids) == 0: try: check_pysonic_connection().deletePlaylist(playlist_id) + logging.info('Fail! No songs found for playlist %s', playlist_name) except DataNotFoundError: pass @@ -241,10 +251,9 @@ def write_playlist(sp, playlist_name, results): 'There was an error creating a Playlist, perhaps is your Subsonic server offline?') -def match_with_subsonic_track(track, artist_spotify, found, - excluded, song_ids, track_helper, playlist_id, playlist_name): +def match_with_subsonic_track(comparison_helper, playlist_id, old_song_ids, playlist_name): """compare spotify track to subsonic one""" - text_to_search = artist_spotify["name"] + " " + track['name'] + text_to_search = comparison_helper.artist_spotify["name"] + " " + comparison_helper.track['name'] subsonic_search_results = get_subsonic_search_results(text_to_search) skipped_songs = [] for song_id in subsonic_search_results: @@ -252,69 +261,70 @@ def match_with_subsonic_track(track, artist_spotify, found, song["isrc-list"] = musicbrainz_helper.get_isrc_by_id(song) placeholder = song["artist"] + " " + \ song["title"] + " " + song["album"] - if song["id"] in song_ids: + if song["id"] in old_song_ids: logging.info( 'Track with id "%s" already in playlist "%s"', song["id"], playlist_name) - found = True - elif (song["id"] not in song_ids + comparison_helper.song_ids.append(song["id"]) + comparison_helper.found = True + elif (song["id"] not in comparison_helper.song_ids and song["artist"] != '' - and track['name'] != '' + and comparison_helper.track['name'] != '' and song["album"] != '' and song["title"] != ''): album_name = "" - if ("album" in track - and "name" in track["album"] - and track["album"]["name"] is not None): - album_name = track["album"]["name"] + if ("album" in comparison_helper.track + and "name" in comparison_helper.track["album"] + and comparison_helper.track["album"]["name"] is not None): + album_name = comparison_helper.track["album"]["name"] logging.info( 'Comparing song "%s - %s - %s" with Spotify track "%s - %s - %s"', song["artist"], song["title"], song["album"], - artist_spotify["name"], - track['name'], + comparison_helper.artist_spotify["name"], + comparison_helper.track['name'], album_name) - if has_isrc(track): + if has_isrc(comparison_helper.track): found_isrc = False for isrc in song["isrc-list"]: - if isrc.strip() == track["external_ids"]["isrc"].strip(): + if isrc.strip() == comparison_helper.track["external_ids"]["isrc"].strip(): found_isrc = True break if found_isrc is True: - song_ids.append(song["id"]) - track_helper.append(placeholder) - found = True + comparison_helper.song_ids.append(song["id"]) + comparison_helper.track_helper.append(placeholder) + comparison_helper.found = True database.insert_song( - dbms, playlist_id, song, artist_spotify, track) + dbms, playlist_id, song, comparison_helper.artist_spotify, comparison_helper.track) logging.info( 'Adding song "%s - %s - %s" to playlist "%s", matched by ISRC: "%s"', song["artist"], song["title"], song["album"], playlist_name, - track["external_ids"]["isrc"]) + comparison_helper.track["external_ids"]["isrc"]) check_pysonic_connection().createPlaylist( - playlistId=playlist_id, songIds=song_ids) + playlistId=playlist_id, songIds=comparison_helper.song_ids) break if (utils.compare_string_to_exclusion(song["title"], utils.get_excluded_words_array()) or utils.compare_string_to_exclusion(song["album"], utils.get_excluded_words_array())): - excluded = True - elif (utils.compare_strings(artist_spotify["name"], song["artist"]) - and utils.compare_strings(track['name'], song["title"]) - and placeholder not in track_helper): - if (("album" in track and "name" in track["album"] - and utils.compare_strings(track['album']['name'], song["album"])) - or ("album" not in track) - or ("album" in track and "name" not in track["album"])): - song_ids.append(song["id"]) - track_helper.append(placeholder) - found = True + comparison_helper.excluded = True + elif (utils.compare_strings(comparison_helper.artist_spotify["name"], song["artist"]) + and utils.compare_strings(comparison_helper.track['name'], song["title"]) + and placeholder not in comparison_helper.track_helper): + if (("album" in comparison_helper.track and "name" in comparison_helper.track["album"] + and utils.compare_strings(comparison_helper.track['album']['name'], song["album"])) + or ("album" not in comparison_helper.track) + or ("album" in comparison_helper.track and "name" not in comparison_helper.track["album"])): + comparison_helper.song_ids.append(song["id"]) + comparison_helper.track_helper.append(placeholder) + comparison_helper.found = True database.insert_song( - dbms, playlist_id, song, artist_spotify, track) + dbms, playlist_id, song, comparison_helper.artist_spotify, comparison_helper.track) logging.info( 'Adding song "%s - %s - %s" to playlist "%s", matched by text comparison', song["artist"], @@ -322,20 +332,20 @@ def match_with_subsonic_track(track, artist_spotify, found, song["album"], playlist_name) check_pysonic_connection().createPlaylist( - playlistId=playlist_id, songIds=song_ids) + playlistId=playlist_id, songIds=comparison_helper.song_ids) break skipped_songs.append(song) - if found is False and excluded is False and len(skipped_songs) > 0: + if comparison_helper.found is False and comparison_helper.excluded is False and len(skipped_songs) > 0: random.shuffle(skipped_songs) for skipped_song in skipped_songs: placeholder = skipped_song["artist"] + " " + \ skipped_song['title'] + " " + skipped_song["album"] - if placeholder not in track_helper: - track_helper.append(placeholder) - song_ids.append(skipped_song["id"]) - found = True + if placeholder not in comparison_helper.track_helper: + comparison_helper.track_helper.append(placeholder) + comparison_helper.song_ids.append(skipped_song["id"]) + comparison_helper.found = True database.insert_song( - dbms, playlist_id, skipped_song, artist_spotify, track) + dbms, playlist_id, skipped_song, comparison_helper.artist_spotify, comparison_helper.track) logging.warning( 'No matching album found for Subsonic search "%s", using a random one', text_to_search) @@ -346,8 +356,8 @@ def match_with_subsonic_track(track, artist_spotify, found, skipped_song["album"], playlist_name) check_pysonic_connection().createPlaylist( - playlistId=playlist_id, songIds=song_ids) - return track, artist_spotify, found, excluded, song_ids, track_helper + playlistId=playlist_id, songIds=comparison_helper.song_ids) + return comparison_helper def get_playlist_songs(missing_only=False): diff --git a/uwsgi.ini b/uwsgi.ini index a545de4..3a39b95 100644 --- a/uwsgi.ini +++ b/uwsgi.ini @@ -25,3 +25,4 @@ route = ^.*healthcheck.*$ donotlog: log-4xx = true log-5xx = true disable-logging = true + \ No newline at end of file From c9a8c9cf2550b8aa139c24c4d501d819c5b981b7 Mon Sep 17 00:00:00 2001 From: blast Date: Sun, 22 Sep 2024 02:07:15 +0200 Subject: [PATCH 02/36] first big refactoring --- main.py | 554 +----------------- spotisub/__init__.py | 26 + spotisub/classes.py | 12 + spotisub/constants.py | 53 ++ spotisub/core/external/classes/classes.py | 8 - .../external/utils/constants/constants.py | 52 -- spotisub/{core/database => }/database.py | 17 +- .../external/exceptions => }/exceptions.py | 5 + .../{generate_playlists.py => generator.py} | 24 +- .../external => helpers}/lidarr_helper.py | 9 +- .../musicbrainz_helper.py | 11 +- .../external => helpers}/spotdl_helper.py | 7 +- .../external => helpers}/spotipy_helper.py | 29 +- spotisub/{core => helpers}/subsonic_helper.py | 80 +-- spotisub/routes.py | 548 +++++++++++++++++ spotisub/{core/external/utils => }/utils.py | 8 +- uwsgi.ini | 2 +- 17 files changed, 734 insertions(+), 711 deletions(-) create mode 100644 spotisub/__init__.py create mode 100644 spotisub/classes.py create mode 100644 spotisub/constants.py delete mode 100644 spotisub/core/external/classes/classes.py delete mode 100644 spotisub/core/external/utils/constants/constants.py rename spotisub/{core/database => }/database.py (97%) rename spotisub/{core/external/exceptions => }/exceptions.py (83%) rename spotisub/{generate_playlists.py => generator.py} (96%) rename spotisub/{core/external => helpers}/lidarr_helper.py (84%) rename spotisub/{core/external => helpers}/musicbrainz_helper.py (77%) rename spotisub/{core/external => helpers}/spotdl_helper.py (76%) rename spotisub/{core/external => helpers}/spotipy_helper.py (72%) rename spotisub/{core => helpers}/subsonic_helper.py (90%) create mode 100644 spotisub/routes.py rename spotisub/{core/external/utils => }/utils.py (95%) diff --git a/main.py b/main.py index 6ae4ebb..4e12b96 100644 --- a/main.py +++ b/main.py @@ -1,561 +1,9 @@ -"""Spotisub main module""" -import logging import os -import random -import threading -import json -from time import strftime - from os.path import dirname from os.path import join from dotenv import load_dotenv -from flask import Flask -from flask import Response -from flask import request -from flask import render_template -from flask_restx import Api -from flask_restx import Resource -from flask_apscheduler import APScheduler -from spotisub.core.external.utils import utils -from spotisub.core.external.utils.constants import constants -from spotisub.core.external import spotipy_helper -from spotisub.core import subsonic_helper -from spotisub import generate_playlists -from spotisub.core.external.exceptions.exceptions import SubsonicOfflineException -from spotisub.core.external.exceptions.exceptions import SpotifyApiException dotenv_path = join(dirname(__file__), '.env') load_dotenv(dotenv_path) -logging.basicConfig( - format='%(asctime)s %(levelname)-8s %(message)s', - level=int( - os.environ.get( - constants.LOG_LEVEL, - constants.LOG_LEVEL_DEFAULT_VALUE)), - datefmt='%Y-%m-%d %H:%M:%S') - -log = logging.getLogger('werkzeug') -log.setLevel(int(os.environ.get(constants.LOG_LEVEL, - constants.LOG_LEVEL_DEFAULT_VALUE))) - -app = Flask(__name__) - -scheduler = APScheduler() - -@app.after_request -def after_request(response): - """Excluding healthcheck endpoint from logging""" - if not request.path.startswith('/utils/healthcheck'): - timestamp = strftime('[%Y-%b-%d %H:%M]') - logging.info('%s %s %s %s %s %s', - timestamp, - request.remote_addr, - request.method, - request.scheme, - request.full_path, - response.status) - return response - - -api = Api(app) - -def get_response_json(data, status): - """Generates json response""" - r = Response(response=data, status=status, mimetype="application/json") - r.headers["Content-Type"] = "application/json; charset=utf-8" - r.headers["Accept"] = "application/json" - return r - - -def get_json_message(message, is_ok): - """Generates json message""" - data = { - "status": "ko" if is_ok is False else "ok", - "message": message, - } - return json.dumps(data) - - -@app.route('/dashboard') -def dashboard(): - """Dashboard path""" - return render_template('dashboard.html', data=[]) - - -nsgenerate = api.namespace('generate', 'Generate APIs') - - -@nsgenerate.route('/artist_recommendations/') -@nsgenerate.route('/artist_recommendations//') -class ArtistRecommendationsClass(Resource): - """Artist reccomendations class""" - def get(self, artist_name=None): - """Artist reccomendations endpoint""" - try: - spotipy_helper.get_secrets() - subsonic_helper.check_pysonic_connection() - if artist_name is None: - artist_name = random.choice( - subsonic_helper.get_artists_array_names()) - else: - search_result_name = subsonic_helper.search_artist(artist_name) - if search_result_name is None: - return get_response_json( - get_json_message( - "Artist " + - artist_name + - " not found, maybe a misspelling error?", - True), - 206) - artist_name = search_result_name - if artist_name is not None: - threading.Thread( - target=lambda: generate_playlists - .show_recommendations_for_artist(artist_name)).start() - return get_response_json( - get_json_message( - "Generating recommendations playlist for artist " + - artist_name, - True), - 200) - return get_response_json( - get_json_message( - "Bad request", - False), - 400) - except SubsonicOfflineException: - utils.write_exception() - return get_response_json( - get_json_message( - "Unable to communicate with Subsonic.", - False), - 400) - except SpotifyApiException: - utils.write_exception() - return get_response_json( - get_json_message( - "Spotify API Error. Please check your configuruation.", - False), - 400) - - -@nsgenerate.route('/artist_recommendations/all/') -class ArtistRecommendationsAllClass(Resource): - """All Artists reccomendations class""" - def get(self): - """All Artists reccomendations endpoint""" - try: - spotipy_helper.get_secrets() - subsonic_helper.check_pysonic_connection() - threading.Thread( - target=lambda: generate_playlists - .all_artists_recommendations(subsonic_helper - .get_artists_array_names())).start() - return get_response_json( - get_json_message( - "Generating recommendations playlist for all artists", - True), - 200) - except SubsonicOfflineException: - utils.write_exception() - return get_response_json( - get_json_message( - "Unable to communicate with Subsonic.", - False), - 400) - except SpotifyApiException: - utils.write_exception() - return get_response_json( - get_json_message( - "Spotify API Error. Please check your configuruation.", - False), - 400) - - -@nsgenerate.route('/artist_top_tracks/') -@nsgenerate.route('/artist_top_tracks//') -class ArtistTopTracksClass(Resource): - """Artist top tracks class""" - def get(self, artist_name=None): - """Artist top tracks endpoint""" - try: - spotipy_helper.get_secrets() - subsonic_helper.check_pysonic_connection() - if artist_name is None: - artist_name = random.choice( - subsonic_helper.get_artists_array_names()) - else: - search_result_name = subsonic_helper.search_artist(artist_name) - if search_result_name is None: - return get_response_json( - get_json_message( - "Artist " + - artist_name + - " not found, maybe a misspelling error?", - True), - 206) - artist_name = search_result_name - if artist_name is not None: - threading.Thread( - target=lambda: generate_playlists.artist_top_tracks(artist_name)).start() - return get_response_json( - get_json_message( - "Generating top tracks playlist for artist " + - artist_name, - True), - 200) - return get_response_json( - get_json_message( - "Bad request", - False), - 400) - except SubsonicOfflineException: - utils.write_exception() - return get_response_json( - get_json_message( - "Unable to communicate with Subsonic.", - False), - 400) - except SpotifyApiException: - utils.write_exception() - return get_response_json( - get_json_message( - "Spotify API Error. Please check your configuruation.", - False), - 400) - - -@nsgenerate.route('/artist_top_tracks/all/') -class ArtistTopTracksAllClass(Resource): - """All Artists top tracks class""" - def get(self): - """All Artists top tracks endpoint""" - try: - spotipy_helper.get_secrets() - subsonic_helper.check_pysonic_connection() - threading.Thread( - target=lambda: generate_playlists - .all_artists_top_tracks(subsonic_helper - .get_artists_array_names())).start() - return get_response_json( - get_json_message( - "Generating top tracks playlist for all artists", - True), - 200) - except SubsonicOfflineException: - utils.write_exception() - return get_response_json( - get_json_message( - "Unable to communicate with Subsonic.", - False), - 400) - except SpotifyApiException: - utils.write_exception() - return get_response_json( - get_json_message( - "Spotify API Error. Please check your configuruation.", - False), - 400) - -@nsgenerate.route('/recommendations') -class RecommendationsClass(Resource): - """Recommendations class""" - def get(self): - """Recommendations endpoint""" - try: - spotipy_helper.get_secrets() - subsonic_helper.check_pysonic_connection() - threading.Thread( - target=lambda: generate_playlists.my_recommendations( - count=random.randrange( - int( - os.environ.get( - constants.NUM_USER_PLAYLISTS, - constants.NUM_USER_PLAYLISTS_DEFAULT_VALUE))))).start() - return get_response_json( - get_json_message( - "Generating a reccommendation playlist", - True), - 200) - except SubsonicOfflineException: - utils.write_exception() - return get_response_json( - get_json_message( - "Unable to communicate with Subsonic.", - False), - 400) - except SpotifyApiException: - utils.write_exception() - return get_response_json( - get_json_message( - "Spotify API Error. Please check your configuruation.", - False), - 400) - - -nsimport = api.namespace('import', 'Generate APIs') - - -@nsimport.route('/user_playlist/') -@nsimport.route('/user_playlist//') -class UserPlaylistsClass(Resource): - """User playlists class""" - def get(self, playlist_name=None): - """User playlists endpoint""" - try: - spotipy_helper.get_secrets() - subsonic_helper.check_pysonic_connection() - if playlist_name is None: - count = generate_playlists.count_user_playlists(0) - threading.Thread( - target=lambda: generate_playlists.get_user_playlists( - random.randrange(count), - single_execution=True)).start() - return get_response_json(get_json_message( - "Importing a random playlist", True), 200) - search_result_name = generate_playlists.get_user_playlist_by_name( - playlist_name) - if search_result_name is None: - return get_response_json( - get_json_message( - "Playlist " + - playlist_name + - " not found, maybe a misspelling error?", - True), - 206) - playlist_name = search_result_name - if playlist_name is not None: - threading.Thread(target=lambda: generate_playlists.get_user_playlists( - 0, single_execution=False, playlist_name=playlist_name)).start() - return get_response_json( - get_json_message( - "Searching and importing your spotify account for playlist " + - playlist_name, - True), - 200) - return get_response_json( - get_json_message( - "Bad request", - False), - 400) - except SubsonicOfflineException: - utils.write_exception() - return get_response_json( - get_json_message( - "Unable to communicate with Subsonic.", - False), - 400) - except SpotifyApiException: - utils.write_exception() - return get_response_json( - get_json_message( - "Spotify API Error. Please check your configuruation.", - False), - 400) - - -@nsimport.route('/user_playlist/all/') -class UserPlaylistsAllClass(Resource): - """All User playlists class""" - def get(self): - """All User playlists endpoint""" - try: - spotipy_helper.get_secrets() - subsonic_helper.check_pysonic_connection() - threading.Thread( - target=lambda: generate_playlists.get_user_playlists(0)).start() - return get_response_json( - get_json_message( - "Importing all your spotify playlists", - True), - 200) - except SubsonicOfflineException: - utils.write_exception() - return get_response_json( - get_json_message( - "Unable to communicate with Subsonic.", - False), - 400) - except SpotifyApiException: - utils.write_exception() - return get_response_json( - get_json_message( - "Spotify API Error. Please check your configuruation.", - False), - 400) - - -@nsimport.route('/saved_tracks') -class SavedTracksClass(Resource): - """Saved tracks class""" - def get(self): - """Saved tracks endpoint""" - try: - spotipy_helper.get_secrets() - subsonic_helper.check_pysonic_connection() - threading.Thread( - target=lambda: generate_playlists - .get_user_saved_tracks(dict({'tracks': []}))).start() - return get_response_json(get_json_message( - "Importing your saved tracks", True), 200) - except SubsonicOfflineException: - utils.write_exception() - return get_response_json( - get_json_message( - "Unable to communicate with Subsonic.", - False), - 400) - except SpotifyApiException: - utils.write_exception() - return get_response_json( - get_json_message( - "Spotify API Error. Please check your configuruation.", - False), - 400) - - -nsdatabase = api.namespace('database', 'Database APIs') - - -@nsdatabase.route('/playlist/unmatched') -class PlaylistUnmatchedClass(Resource): - """Unmatched playlist songs class""" - def get(self): - """Unmatched playlist songs endpoint""" - try: - spotipy_helper.get_secrets() - subsonic_helper.check_pysonic_connection() - missing_songs = subsonic_helper.get_playlist_songs( - missing_only=True) - return get_response_json(json.dumps(missing_songs), 200) - except SubsonicOfflineException: - utils.write_exception() - return get_response_json( - get_json_message( - "Unable to communicate with Subsonic.", - False), - 400) - except SpotifyApiException: - utils.write_exception() - return get_response_json( - get_json_message( - "Spotify API Error. Please check your configuruation.", - False), - 400) - - -nsdatabase = api.namespace('database', 'Database APIs') - - -@nsdatabase.route('/playlist/all') -class PlaylistAllClass(Resource): - """All playlist songs class""" - def get(self): - """All playlist songs endpoint""" - try: - spotipy_helper.get_secrets() - subsonic_helper.check_pysonic_connection() - missing_songs = subsonic_helper.get_playlist_songs( - missing_only=False) - return get_response_json(json.dumps(missing_songs), 200) - except SubsonicOfflineException: - utils.write_exception() - return get_response_json( - get_json_message( - "Unable to communicate with Subsonic.", - False), - 400) - except SpotifyApiException: - utils.write_exception() - return get_response_json( - get_json_message( - "Spotify API Error. Please check your configuruation.", - False), - 400) - - -nsutils = api.namespace('utils', 'Utils APIs') - - -@nsutils.route('/healthcheck') -class Healthcheck(Resource): - """Healthcheck class""" - def get(self): - """Healthcheck endpoint""" - return "Ok!" - - -if os.environ.get(constants.SCHEDULER_ENABLED, - constants.SCHEDULER_ENABLED_DEFAULT_VALUE) == "1": - - if os.environ.get(constants.ARTIST_GEN_SCHED, - constants.ARTIST_GEN_SCHED_DEFAULT_VALUE) != "0": - @scheduler.task('interval', - id='artist_recommendations', - hours=int(os.environ.get(constants.ARTIST_GEN_SCHED, - constants.ARTIST_GEN_SCHED_DEFAULT_VALUE))) - def artist_recommendations(): - """artist_recommendations task""" - generate_playlists.show_recommendations_for_artist( - random.choice(subsonic_helper.get_artists_array_names())) - - if os.environ.get(constants.ARTIST_TOP_GEN_SCHED, - constants.ARTIST_TOP_GEN_SCHED_DEFAULT_VALUE) != "0": - @scheduler.task('interval', - id='artist_top_tracks', - hours=int(os.environ.get(constants.ARTIST_TOP_GEN_SCHED, - constants.ARTIST_TOP_GEN_SCHED_DEFAULT_VALUE))) - def artist_top_tracks(): - """artist_top_tracks task""" - generate_playlists.artist_top_tracks( - random.choice(subsonic_helper.get_artists_array_names())) - - if os.environ.get(constants.RECOMEND_GEN_SCHED, - constants.RECOMEND_GEN_SCHED_DEFAULT_VALUE) != "0": - @scheduler.task('interval', - id='my_recommendations', - hours=int(os.environ.get(constants.RECOMEND_GEN_SCHED, - constants.RECOMEND_GEN_SCHED_DEFAULT_VALUE))) - def my_recommendations(): - """my_recommendations task""" - generate_playlists.my_recommendations(count=random.randrange(int(os.environ.get( - constants.NUM_USER_PLAYLISTS, constants.NUM_USER_PLAYLISTS_DEFAULT_VALUE)))) - - if os.environ.get(constants.PLAYLIST_GEN_SCHED, - constants.PLAYLIST_GEN_SCHED_DEFAULT_VALUE) != "0": - @scheduler.task('interval', - id='user_playlists', - hours=int(os.environ.get(constants.PLAYLIST_GEN_SCHED, - constants.PLAYLIST_GEN_SCHED_DEFAULT_VALUE))) - def user_playlists(): - """user_playlists task""" - generate_playlists.get_user_playlists( - random.randrange( - generate_playlists.count_user_playlists(0)), - single_execution=True) - - if os.environ.get(constants.SAVED_GEN_SCHED, - constants.SAVED_GEN_SCHED_DEFAULT_VALUE) != "0": - @scheduler.task('interval', - id='saved_tracks', - hours=int(os.environ.get(constants.SAVED_GEN_SCHED, - constants.SAVED_GEN_SCHED_DEFAULT_VALUE))) - def saved_tracks(): - """saved_tracks task""" - generate_playlists.get_user_saved_tracks(dict({'tracks': []})) - - -@scheduler.task('interval', id='remove_subsonic_deleted_playlist', hours=12) -def remove_subsonic_deleted_playlist(): - """remove_subsonic_deleted_playlist task""" - subsonic_helper.remove_subsonic_deleted_playlist() - - -scheduler.init_app(app) -scheduler.start() - -utils.print_logo(constants.VERSION) - -if __name__ == '__main__': - app.run() +import spotisub \ No newline at end of file diff --git a/spotisub/__init__.py b/spotisub/__init__.py new file mode 100644 index 0000000..6722d71 --- /dev/null +++ b/spotisub/__init__.py @@ -0,0 +1,26 @@ +"""Spotisub init module""" +import logging +import os + +from flask import Flask +from spotisub import constants +from spotisub import utils + +utils.print_logo(constants.VERSION) + +logging.basicConfig( + format='%(asctime)s %(levelname)-8s %(message)s', + level=int( + os.environ.get( + constants.LOG_LEVEL, + constants.LOG_LEVEL_DEFAULT_VALUE)), + datefmt='%Y-%m-%d %H:%M:%S') + +log = logging.getLogger('werkzeug') +log.setLevel(int(os.environ.get(constants.LOG_LEVEL, + constants.LOG_LEVEL_DEFAULT_VALUE))) + + +spotisub = Flask(__name__) + +from spotisub import routes \ No newline at end of file diff --git a/spotisub/classes.py b/spotisub/classes.py new file mode 100644 index 0000000..142016c --- /dev/null +++ b/spotisub/classes.py @@ -0,0 +1,12 @@ +"""Spotisub classes""" + + +class ComparisonHelper: + def __init__(self, track, artist_spotify, found, + excluded, song_ids, track_helper): + self.track = track + self.artist_spotify = artist_spotify + self.found = found + self.excluded = excluded + self.song_ids = song_ids + self.track_helper = track_helper diff --git a/spotisub/constants.py b/spotisub/constants.py new file mode 100644 index 0000000..da31136 --- /dev/null +++ b/spotisub/constants.py @@ -0,0 +1,53 @@ +"""Spotisub constants""" +VERSION = "0.3.0-alpha" +SPLIT_TOKENS = ["(", "-", "feat"] + +ARTIST_GEN_SCHED = "ARTIST_GEN_SCHED" +ARTIST_TOP_GEN_SCHED = "ARTIST_GEN_SCHED" +EXCLUDED_WORDS = "EXCLUDED_WORDS" +ITEMS_PER_PLAYLIST = "ITEMS_PER_PLAYLIST" +LIDARR_BASE_API_PATH = "LIDARR_BASE_API_PATH" +LIDARR_ENABLED = "LIDARR_ENABLED" +LIDARR_IP = "LIDARR_IP" +LIDARR_PORT = "LIDARR_PORT" +LIDARR_TOKEN = "LIDARR_TOKEN" +LIDARR_USE_SSL = "LIDARR_USE_SSL" +LOG_LEVEL = "LOG_LEVEL" +NUM_USER_PLAYLISTS = "NUM_USER_PLAYLISTS" +PLAYLIST_GEN_SCHED = "PLAYLIST_GEN_SCHED" +PLAYLIST_PREFIX = "PLAYLIST_PREFIX" +RECOMEND_GEN_SCHED = "RECOMEND_GEN_SCHED" +SAVED_GEN_SCHED = "SAVED_GEN_SCHED" +SCHEDULER_ENABLED = "SCHEDULER_ENABLED" +SPOTDL_ENABLED = "SPOTDL_ENABLED" +SPOTDL_FORMAT = "SPOTDL_FORMAT" +SPOTIPY_CLIENT_ID = "SPOTIPY_CLIENT_ID" +SPOTIPY_CLIENT_SECRET = "SPOTIPY_CLIENT_SECRET" +SPOTIPY_REDIRECT_URI = "SPOTIPY_REDIRECT_URI" +SUBSONIC_API_BASE_URL = "SUBSONIC_API_BASE_URL" +SUBSONIC_API_HOST = "SUBSONIC_API_HOST" +SUBSONIC_API_USER = "SUBSONIC_API_USER" +SUBSONIC_API_PASS = "SUBSONIC_API_PASS" +SUBSONIC_API_PORT = "SUBSONIC_API_PORT" + + +ARTIST_GEN_SCHED_DEFAULT_VALUE = "1" +ARTIST_TOP_GEN_SCHED_DEFAULT_VALUE = "1" +EXCLUDED_WORDS_DEFAULT_VALUE = "acoustic,instrumental,demo" +ITEMS_PER_PLAYLIST_DEFAULT_VALUE = "1000" +LIDARR_BASE_API_PATH_DEFAULT_VALUE = "" +LIDARR_ENABLED_DEFAULT_VALUE = "0" +LIDARR_USE_SSL_DEFAULT_VALUE = "0" +LOG_LEVEL_DEFAULT_VALUE = "40" +NUM_USER_PLAYLISTS_DEFAULT_VALUE = "5" +PLAYLIST_GEN_SCHED_DEFAULT_VALUE = "3" +PLAYLIST_PREFIX_DEFAULT_VALUE = "Spotisub - " +RECOMEND_GEN_SCHED_DEFAULT_VALUE = "4" +SAVED_GEN_SCHED_DEFAULT_VALUE = "2" +SCHEDULER_ENABLED_DEFAULT_VALUE = "1" +SPOTDL_ENABLED_DEFAULT_VALUE = "0" +SPOTDL_FORMAT_DEFAULT_VALUE = "/music/{artist}/{artists} - {album} ({year}) - {track-number} - {title}.{output-ext}" +SPOTIPY_CLIENT_ID_DEFAULT_VALUE = "" +SPOTIPY_CLIENT_SECRET_DEFAULT_VALUE = "" +SPOTIPY_REDIRECT_URI_DEFAULT_VALUE = "http://127.0.0.1:8080/" +SUBSONIC_API_BASE_URL_DEFAULT_VALUE = "" diff --git a/spotisub/core/external/classes/classes.py b/spotisub/core/external/classes/classes.py deleted file mode 100644 index 0707cba..0000000 --- a/spotisub/core/external/classes/classes.py +++ /dev/null @@ -1,8 +0,0 @@ -class ComparisonHelper: - def __init__(self, track, artist_spotify, found, excluded, song_ids, track_helper): - self.track = track - self.artist_spotify = artist_spotify - self.found = found - self.excluded = excluded - self.song_ids = song_ids - self.track_helper = track_helper \ No newline at end of file diff --git a/spotisub/core/external/utils/constants/constants.py b/spotisub/core/external/utils/constants/constants.py deleted file mode 100644 index d0cd368..0000000 --- a/spotisub/core/external/utils/constants/constants.py +++ /dev/null @@ -1,52 +0,0 @@ -VERSION = "0.2.5" -SPLIT_TOKENS = ["(", "-", "feat"] - -ARTIST_GEN_SCHED = "ARTIST_GEN_SCHED" -ARTIST_TOP_GEN_SCHED = "ARTIST_GEN_SCHED" -EXCLUDED_WORDS = "EXCLUDED_WORDS" -ITEMS_PER_PLAYLIST = "ITEMS_PER_PLAYLIST" -LIDARR_BASE_API_PATH = "LIDARR_BASE_API_PATH" -LIDARR_ENABLED = "LIDARR_ENABLED" -LIDARR_IP = "LIDARR_IP" -LIDARR_PORT = "LIDARR_PORT" -LIDARR_TOKEN = "LIDARR_TOKEN" -LIDARR_USE_SSL = "LIDARR_USE_SSL" -LOG_LEVEL = "LOG_LEVEL" -NUM_USER_PLAYLISTS = "NUM_USER_PLAYLISTS" -PLAYLIST_GEN_SCHED = "PLAYLIST_GEN_SCHED" -PLAYLIST_PREFIX = "PLAYLIST_PREFIX" -RECOMEND_GEN_SCHED = "RECOMEND_GEN_SCHED" -SAVED_GEN_SCHED = "SAVED_GEN_SCHED" -SCHEDULER_ENABLED = "SCHEDULER_ENABLED" -SPOTDL_ENABLED = "SPOTDL_ENABLED" -SPOTDL_FORMAT = "SPOTDL_FORMAT" -SPOTIPY_CLIENT_ID = "SPOTIPY_CLIENT_ID" -SPOTIPY_CLIENT_SECRET = "SPOTIPY_CLIENT_SECRET" -SPOTIPY_REDIRECT_URI = "SPOTIPY_REDIRECT_URI" -SUBSONIC_API_BASE_URL = "SUBSONIC_API_BASE_URL" -SUBSONIC_API_HOST = "SUBSONIC_API_HOST" -SUBSONIC_API_USER = "SUBSONIC_API_USER" -SUBSONIC_API_PASS = "SUBSONIC_API_PASS" -SUBSONIC_API_PORT = "SUBSONIC_API_PORT" - - -ARTIST_GEN_SCHED_DEFAULT_VALUE = "1" -ARTIST_TOP_GEN_SCHED_DEFAULT_VALUE = "1" -EXCLUDED_WORDS_DEFAULT_VALUE = "acoustic,instrumental,demo" -ITEMS_PER_PLAYLIST_DEFAULT_VALUE = "1000" -LIDARR_BASE_API_PATH_DEFAULT_VALUE = "" -LIDARR_ENABLED_DEFAULT_VALUE = "0" -LIDARR_USE_SSL_DEFAULT_VALUE = "0" -LOG_LEVEL_DEFAULT_VALUE = "40" -NUM_USER_PLAYLISTS_DEFAULT_VALUE = "5" -PLAYLIST_GEN_SCHED_DEFAULT_VALUE = "3" -PLAYLIST_PREFIX_DEFAULT_VALUE = "Spotisub - " -RECOMEND_GEN_SCHED_DEFAULT_VALUE = "4" -SAVED_GEN_SCHED_DEFAULT_VALUE = "2" -SCHEDULER_ENABLED_DEFAULT_VALUE = "1" -SPOTDL_ENABLED_DEFAULT_VALUE = "0" -SPOTDL_FORMAT_DEFAULT_VALUE = "/music/{artist}/{artists} - {album} ({year}) - {track-number} - {title}.{output-ext}" -SPOTIPY_CLIENT_ID_DEFAULT_VALUE = "" -SPOTIPY_CLIENT_SECRET_DEFAULT_VALUE = "" -SPOTIPY_REDIRECT_URI_DEFAULT_VALUE = "http://127.0.0.1:8080/" -SUBSONIC_API_BASE_URL_DEFAULT_VALUE = "" diff --git a/spotisub/core/database/database.py b/spotisub/database.py similarity index 97% rename from spotisub/core/database/database.py rename to spotisub/database.py index 09f65df..bf60204 100644 --- a/spotisub/core/database/database.py +++ b/spotisub/database.py @@ -12,6 +12,7 @@ from sqlalchemy.sql import func SQLITE = 'sqlite' +USER = 'user' SUBSONIC_PLAYLIST = 'subsonic_playlist' SUBSONIC_SONG = 'subsonic_song' SUBSONIC_ARTIST = 'subsonic_artist' @@ -39,6 +40,15 @@ def __init__(self, dbtype, dbname=''): metadata = MetaData() + user = Table(USER, metadata, + Column( + 'id', String(36), primary_key=True, nullable=False), + Column( + 'username', String(36), unique=True, index=True, nullable=False), + Column( + 'password_hash', String(128), nullable=False) + ) + subsonic_spotify_relation = Table(SUBSONIC_SPOTIFY_RELATION, metadata, Column( 'uuid', String(36), primary_key=True, nullable=False), @@ -151,9 +161,10 @@ def delete_playlist_relation_by_id(self, playlist_id: str): conn.commit() conn.close() + def delete_song_relation(self, playlist_id: str, subsonic_track): """delete playlist from database""" - + if "id" in subsonic_track: stmt = None if "artistId" in subsonic_track: @@ -200,8 +211,8 @@ def select_all_playlists(self, missing_only): self.subsonic_spotify_relation.c.subsonic_artist_id, self.subsonic_spotify_relation.c.subsonic_playlist_id, self.subsonic_spotify_relation.c.spotify_song_uuid).where( - self.subsonic_spotify_relation.c.subsonic_song_id == None, - self.subsonic_spotify_relation.c.subsonic_artist_id == None) + self.subsonic_spotify_relation.c.subsonic_song_id is None, + self.subsonic_spotify_relation.c.subsonic_artist_id is None) else: stmt = select( self.subsonic_spotify_relation.c.uuid, diff --git a/spotisub/core/external/exceptions/exceptions.py b/spotisub/exceptions.py similarity index 83% rename from spotisub/core/external/exceptions/exceptions.py rename to spotisub/exceptions.py index 1eca796..cf8afab 100644 --- a/spotisub/core/external/exceptions/exceptions.py +++ b/spotisub/exceptions.py @@ -1,4 +1,9 @@ +"""Spotisub exceptions""" + + class SpotifyApiException(Exception): "Please set up your Spotify API Keys" + + class SubsonicOfflineException(Exception): "Subsonic is Offline" diff --git a/spotisub/generate_playlists.py b/spotisub/generator.py similarity index 96% rename from spotisub/generate_playlists.py rename to spotisub/generator.py index 09ba296..7368771 100644 --- a/spotisub/generate_playlists.py +++ b/spotisub/generator.py @@ -1,18 +1,22 @@ -"""Subsonic helper""" +"""Subsonic generator""" import logging import os import random import time import re -from os.path import dirname -from os.path import join -from dotenv import load_dotenv -from .core.external.utils.constants import constants -from .core.external import spotipy_helper -from .core import subsonic_helper - -dotenv_path = join(dirname(__file__), '.env') -load_dotenv(dotenv_path) +from spotisub import constants +from spotisub.helpers import spotipy_helper +from spotisub.helpers import subsonic_helper + + +def get_spotipy_helper(): + """get spotipy helper""" + return spotipy_helper + + +def get_subsonic_helper(): + """get subsonic helper""" + return subsonic_helper def artist_top_tracks(query): diff --git a/spotisub/core/external/lidarr_helper.py b/spotisub/helpers/lidarr_helper.py similarity index 84% rename from spotisub/core/external/lidarr_helper.py rename to spotisub/helpers/lidarr_helper.py index 7afb10b..23b89c9 100644 --- a/spotisub/core/external/lidarr_helper.py +++ b/spotisub/helpers/lidarr_helper.py @@ -1,15 +1,10 @@ """Lidarr helper""" import os -from os.path import dirname -from os.path import join -from dotenv import load_dotenv from pyarr import LidarrAPI from expiringdict import ExpiringDict -from .utils import utils -from .utils.constants import constants -dotenv_path = join(dirname(__file__), '.env') -load_dotenv(dotenv_path) +from spotisub import utils +from spotisub import constants ipaddress = os.environ.get(constants.LIDARR_IP) port = os.environ.get(constants.LIDARR_PORT) diff --git a/spotisub/core/external/musicbrainz_helper.py b/spotisub/helpers/musicbrainz_helper.py similarity index 77% rename from spotisub/core/external/musicbrainz_helper.py rename to spotisub/helpers/musicbrainz_helper.py index 499d298..2e8205c 100644 --- a/spotisub/core/external/musicbrainz_helper.py +++ b/spotisub/helpers/musicbrainz_helper.py @@ -3,13 +3,8 @@ import logging -from os.path import dirname -from os.path import join -from dotenv import load_dotenv import musicbrainzngs -from .utils import utils -dotenv_path = join(dirname(__file__), '.env') -load_dotenv(dotenv_path) +from spotisub import utils # Disabling musicbrainz INFO log as we don't want to see ugly infos in the @@ -28,7 +23,7 @@ def get_isrc_by_id(song): try: if ("musicBrainzId" in song and song["musicBrainzId"] is not None - and song["musicBrainzId"] != ""): + and song["musicBrainzId"] != ""): song = musicbrainzngs.get_recording_by_id( song["musicBrainzId"], includes=["isrcs"]) time.sleep(1) @@ -36,7 +31,7 @@ def get_isrc_by_id(song): and song["recording"] is not None and "isrc-list" in song["recording"] and song["recording"]["isrc-list"] is not None - and len(song["recording"]["isrc-list"])) > 0: + and len(song["recording"]["isrc-list"])) > 0: return song["recording"]["isrc-list"] return [] except BaseException: diff --git a/spotisub/core/external/spotdl_helper.py b/spotisub/helpers/spotdl_helper.py similarity index 76% rename from spotisub/core/external/spotdl_helper.py rename to spotisub/helpers/spotdl_helper.py index 0cb473a..c8767b9 100644 --- a/spotisub/core/external/spotdl_helper.py +++ b/spotisub/helpers/spotdl_helper.py @@ -1,15 +1,10 @@ """Spotdl helper""" import os -from os.path import dirname -from os.path import join -from dotenv import load_dotenv from spotdl import Spotdl from spotdl.types.song import Song -from .utils.constants import constants +from spotisub import constants -dotenv_path = join(dirname(__file__), '.env') -load_dotenv(dotenv_path) client_id = os.environ.get(constants.SPOTIPY_CLIENT_ID) client_secret = os.environ.get(constants.SPOTIPY_CLIENT_SECRET) diff --git a/spotisub/core/external/spotipy_helper.py b/spotisub/helpers/spotipy_helper.py similarity index 72% rename from spotisub/core/external/spotipy_helper.py rename to spotisub/helpers/spotipy_helper.py index 5b327db..48e50e7 100644 --- a/spotisub/core/external/spotipy_helper.py +++ b/spotisub/helpers/spotipy_helper.py @@ -1,19 +1,13 @@ """Spotipy helper""" import os -from os.path import dirname -from os.path import join -from dotenv import load_dotenv import spotipy from spotipy import SpotifyOAuth -from .utils import utils -from .utils.constants import constants -from .exceptions.exceptions import SpotifyApiException +from spotisub import constants +from spotisub.exceptions import SpotifyApiException -dotenv_path = join(dirname(__file__), '.env') -load_dotenv(dotenv_path) +SP = None -sp = None def get_secrets(): """Get Spotify api keys from env vars""" @@ -37,24 +31,27 @@ def get_secrets(): return secrets raise SpotifyApiException() + def create_sp_client(): """Creates the spotipy client""" secrets = get_secrets() - SCOPE = "user-top-read,user-library-read,user-read-recently-played" - creds = spotipy.SpotifyOAuth( - scope=SCOPE, + scope = "user-top-read,user-library-read,user-read-recently-played" + creds = SpotifyOAuth( + scope=scope, client_id=secrets["client_id"], client_secret=secrets["client_secret"], redirect_uri=secrets["redirect_uri"], open_browser=False, cache_path=os.path.dirname( os.path.abspath(__file__)) + - "/../../../cache/spotipy_cache") + "/../../cache/spotipy_cache") return spotipy.Spotify(auth_manager=creds) - + + def get_spotipy_client(): """Get the previously created spotipy client""" - return sp + return SP + -sp = create_sp_client() \ No newline at end of file +SP = create_sp_client() diff --git a/spotisub/core/subsonic_helper.py b/spotisub/helpers/subsonic_helper.py similarity index 90% rename from spotisub/core/subsonic_helper.py rename to spotisub/helpers/subsonic_helper.py index c1db191..4085216 100644 --- a/spotisub/core/subsonic_helper.py +++ b/spotisub/helpers/subsonic_helper.py @@ -3,39 +3,33 @@ import os import random import time -from os.path import dirname -from os.path import join -from dotenv import load_dotenv import libsonic from libsonic.errors import DataNotFoundError -from .external.utils.constants import constants -from .external.utils import utils -from .external.exceptions.exceptions import SubsonicOfflineException -from .database import database -from .external import musicbrainz_helper -from .external.classes.classes import ComparisonHelper +from spotisub import constants +from spotisub import utils +from spotisub.exceptions import SubsonicOfflineException +from spotisub import database +from spotisub.classes import ComparisonHelper +from spotisub.helpers import musicbrainz_helper if os.environ.get(constants.SPOTDL_ENABLED, constants.SPOTDL_ENABLED_DEFAULT_VALUE) == "1": - from .external import spotdl_helper + from spotisub.helpers import spotdl_helper logging.warning( "You have enabled SPOTDL integration, " + - "make sure to configure the correct download " + + "make sure to configure the correct download " + "path and check that you have enough disk space " + "for music downloading.") if os.environ.get(constants.LIDARR_ENABLED, constants.LIDARR_ENABLED_DEFAULT_VALUE) == "1": - from .external import lidarr_helper + from spotisub.helpers import lidarr_helper logging.warning( "You have enabled LIDARR integration, " + "if an artist won't be found inside the " + "lidarr database, the download process will be skipped.") -dotenv_path = join(dirname(__file__), '.env') -load_dotenv(dotenv_path) - dbms = database.Database(database.SQLITE, dbname='spotisub.sqlite3') database.create_db_tables(dbms) @@ -99,7 +93,7 @@ def get_subsonic_search_results(text_to_search): subsonic_search = check_pysonic_connection().search2(set_search) if ("searchResult2" in subsonic_search and len(subsonic_search["searchResult2"]) > 0 - and "song" in subsonic_search["searchResult2"]): + and "song" in subsonic_search["searchResult2"]): for song in subsonic_search["searchResult2"]["song"]: if "id" in song and song["id"] not in result: result[song["id"]] = song @@ -179,17 +173,17 @@ def write_playlist(sp, playlist_name, results): track['name']) if "name" in track: comparison_helper = ComparisonHelper(track, - artist_spotify, - found, - excluded, - song_ids, - track_helper) + artist_spotify, + found, + excluded, + song_ids, + track_helper) comparison_helper = match_with_subsonic_track( comparison_helper, playlist_id, old_song_ids, playlist_name) - + track = comparison_helper.track artist_spotify = comparison_helper.artist_spotify found = comparison_helper.found @@ -198,12 +192,12 @@ def write_playlist(sp, playlist_name, results): track_helper = comparison_helper.track_helper if not excluded: if (os.environ.get(constants.SPOTDL_ENABLED, - constants.SPOTDL_ENABLED_DEFAULT_VALUE) == "1" + constants.SPOTDL_ENABLED_DEFAULT_VALUE) == "1" and found is False): if "external_urls" in track and "spotify" in track["external_urls"]: is_monitored = True if (os.environ.get(constants.LIDARR_ENABLED, - constants.LIDARR_ENABLED_DEFAULT_VALUE) == "1"): + constants.LIDARR_ENABLED_DEFAULT_VALUE) == "1"): is_monitored = lidarr_helper.is_artist_monitored( artist_spotify["name"]) if is_monitored: @@ -234,7 +228,7 @@ def write_playlist(sp, playlist_name, results): database.insert_song( dbms, playlist_id, None, artist_spotify, track) if playlist_id is not None: - + if len(song_ids) > 0: check_pysonic_connection().createPlaylist( playlistId=playlist_id, songIds=song_ids) @@ -242,7 +236,9 @@ def write_playlist(sp, playlist_name, results): elif len(song_ids) == 0: try: check_pysonic_connection().deletePlaylist(playlist_id) - logging.info('Fail! No songs found for playlist %s', playlist_name) + logging.info( + 'Fail! No songs found for playlist %s', + playlist_name) except DataNotFoundError: pass @@ -251,9 +247,11 @@ def write_playlist(sp, playlist_name, results): 'There was an error creating a Playlist, perhaps is your Subsonic server offline?') -def match_with_subsonic_track(comparison_helper, playlist_id, old_song_ids, playlist_name): +def match_with_subsonic_track( + comparison_helper, playlist_id, old_song_ids, playlist_name): """compare spotify track to subsonic one""" - text_to_search = comparison_helper.artist_spotify["name"] + " " + comparison_helper.track['name'] + text_to_search = comparison_helper.artist_spotify["name"] + \ + " " + comparison_helper.track['name'] subsonic_search_results = get_subsonic_search_results(text_to_search) skipped_songs = [] for song_id in subsonic_search_results: @@ -269,14 +267,14 @@ def match_with_subsonic_track(comparison_helper, playlist_id, old_song_ids, play comparison_helper.song_ids.append(song["id"]) comparison_helper.found = True elif (song["id"] not in comparison_helper.song_ids - and song["artist"] != '' - and comparison_helper.track['name'] != '' - and song["album"] != '' - and song["title"] != ''): + and song["artist"] != '' + and comparison_helper.track['name'] != '' + and song["album"] != '' + and song["title"] != ''): album_name = "" if ("album" in comparison_helper.track and "name" in comparison_helper.track["album"] - and comparison_helper.track["album"]["name"] is not None): + and comparison_helper.track["album"]["name"] is not None): album_name = comparison_helper.track["album"]["name"] logging.info( 'Comparing song "%s - %s - %s" with Spotify track "%s - %s - %s"', @@ -289,7 +287,8 @@ def match_with_subsonic_track(comparison_helper, playlist_id, old_song_ids, play if has_isrc(comparison_helper.track): found_isrc = False for isrc in song["isrc-list"]: - if isrc.strip() == comparison_helper.track["external_ids"]["isrc"].strip(): + if isrc.strip( + ) == comparison_helper.track["external_ids"]["isrc"].strip(): found_isrc = True break if found_isrc is True: @@ -311,7 +310,7 @@ def match_with_subsonic_track(comparison_helper, playlist_id, old_song_ids, play if (utils.compare_string_to_exclusion(song["title"], utils.get_excluded_words_array()) or utils.compare_string_to_exclusion(song["album"], - utils.get_excluded_words_array())): + utils.get_excluded_words_array())): comparison_helper.excluded = True elif (utils.compare_strings(comparison_helper.artist_spotify["name"], song["artist"]) and utils.compare_strings(comparison_helper.track['name'], song["title"]) @@ -335,7 +334,8 @@ def match_with_subsonic_track(comparison_helper, playlist_id, old_song_ids, play playlistId=playlist_id, songIds=comparison_helper.song_ids) break skipped_songs.append(song) - if comparison_helper.found is False and comparison_helper.excluded is False and len(skipped_songs) > 0: + if comparison_helper.found is False and comparison_helper.excluded is False and len( + skipped_songs) > 0: random.shuffle(skipped_songs) for skipped_song in skipped_songs: placeholder = skipped_song["artist"] + " " + \ @@ -383,7 +383,7 @@ def get_playlist_songs(missing_only=False): missings = playlist_songs_db[key] for missing in missings: if ("subsonic_playlist_id" in missing - and missing["subsonic_playlist_id"] is not None): + and missing["subsonic_playlist_id"] is not None): if "playlist" in playlist_search: single_playlist_search = playlist_search["playlist"] @@ -391,14 +391,14 @@ def get_playlist_songs(missing_only=False): try: if ("subsonic_artist_id" in missing - and missing["subsonic_artist_id"] is not None): + and missing["subsonic_artist_id"] is not None): artist_search = check_pysonic_connection().getArtist( missing["subsonic_artist_id"]) if "artist" in artist_search: single_artist_search = artist_search["artist"] missing["subsonic_artist_name"] = single_artist_search["name"] if ("subsonic_song_id" in missing - and missing["subsonic_song_id"] is not None): + and missing["subsonic_song_id"] is not None): song_search = check_pysonic_connection().getSong( missing["subsonic_song_id"]) if "song" in song_search: @@ -415,7 +415,7 @@ def get_playlist_songs(missing_only=False): if found_error: logging.warning( - 'Found a song inside Spotisub playlist %s ' + + 'Found a song inside Spotisub playlist %s ' + 'with an unmatched Subsonic entry. ' + 'Deleting this playlist.', single_playlist_search["name"]) diff --git a/spotisub/routes.py b/spotisub/routes.py new file mode 100644 index 0000000..c5c0572 --- /dev/null +++ b/spotisub/routes.py @@ -0,0 +1,548 @@ +"""Spotisub routes module""" +import logging +import os +import random +import threading +import json +from time import strftime +from flask import Response +from flask import request +from flask import render_template +from flask_restx import Api +from flask_restx import Resource +from flask_apscheduler import APScheduler +from spotisub import spotisub +from spotisub import utils +from spotisub import constants +from spotisub import generator +from spotisub.generator import get_subsonic_helper +from spotisub.generator import get_spotipy_helper +from spotisub.exceptions import SubsonicOfflineException +from spotisub.exceptions import SpotifyApiException + + +@spotisub.after_request +def after_request(response): + """Excluding healthcheck endpoint from logging""" + if not request.path.startswith('/utils/healthcheck'): + timestamp = strftime('[%Y-%b-%d %H:%M]') + logging.info('%s %s %s %s %s %s', + timestamp, + request.remote_addr, + request.method, + request.scheme, + request.full_path, + response.status) + return response + + +api = Api(spotisub) + + +def get_response_json(data, status): + """Generates json response""" + r = Response(response=data, status=status, mimetype="application/json") + r.headers["Content-Type"] = "application/json; charset=utf-8" + r.headers["Accept"] = "application/json" + return r + + +def get_json_message(message, is_ok): + """Generates json message""" + data = { + "status": "ko" if is_ok is False else "ok", + "message": message, + } + return json.dumps(data) + + +@spotisub.route('/dashboard') +def dashboard(): + """Dashboard path""" + return render_template('dashboard.html', data=[]) + + +nsgenerate = api.namespace('generate', 'Generate APIs') + + +@nsgenerate.route('/artist_recommendations/') +@nsgenerate.route('/artist_recommendations//') +class ArtistRecommendationsClass(Resource): + """Artist reccomendations class""" + def get(self, artist_name=None): + """Artist reccomendations endpoint""" + try: + get_spotipy_helper().get_secrets() + get_subsonic_helper().check_pysonic_connection() + if artist_name is None: + artist_name = random.choice( + get_subsonic_helper().get_artists_array_names()) + else: + search_result_name = get_subsonic_helper().search_artist(artist_name) + if search_result_name is None: + return get_response_json( + get_json_message( + "Artist " + + artist_name + + " not found, maybe a misspelling error?", + True), + 206) + artist_name = search_result_name + if artist_name is not None: + threading.Thread( + target=lambda: generator + .show_recommendations_for_artist(artist_name)).start() + return get_response_json( + get_json_message( + "Generating recommendations playlist for artist " + + artist_name, + True), + 200) + return get_response_json( + get_json_message( + "Bad request", + False), + 400) + except SubsonicOfflineException: + utils.write_exception() + return get_response_json( + get_json_message( + "Unable to communicate with Subsonic.", + False), + 400) + except SpotifyApiException: + utils.write_exception() + return get_response_json( + get_json_message( + "Spotify API Error. Please check your configuruation.", + False), + 400) + + +@nsgenerate.route('/artist_recommendations/all/') +class ArtistRecommendationsAllClass(Resource): + """All Artists reccomendations class""" + + def get(self): + """All Artists reccomendations endpoint""" + try: + get_spotipy_helper().get_secrets() + get_subsonic_helper().check_pysonic_connection() + threading.Thread( + target=lambda: generator + .all_artists_recommendations(get_subsonic_helper() + .get_artists_array_names())).start() + return get_response_json( + get_json_message( + "Generating recommendations playlist for all artists", + True), + 200) + except SubsonicOfflineException: + utils.write_exception() + return get_response_json( + get_json_message( + "Unable to communicate with Subsonic.", + False), + 400) + except SpotifyApiException: + utils.write_exception() + return get_response_json( + get_json_message( + "Spotify API Error. Please check your configuruation.", + False), + 400) + + +@nsgenerate.route('/artist_top_tracks/') +@nsgenerate.route('/artist_top_tracks//') +class ArtistTopTracksClass(Resource): + """Artist top tracks class""" + + def get(self, artist_name=None): + """Artist top tracks endpoint""" + try: + get_spotipy_helper().get_secrets() + get_subsonic_helper().check_pysonic_connection() + if artist_name is None: + artist_name = random.choice( + get_subsonic_helper().get_artists_array_names()) + else: + search_result_name = get_subsonic_helper().search_artist(artist_name) + if search_result_name is None: + return get_response_json( + get_json_message( + "Artist " + + artist_name + + " not found, maybe a misspelling error?", + True), + 206) + artist_name = search_result_name + if artist_name is not None: + threading.Thread( + target=lambda: generator.artist_top_tracks(artist_name)).start() + return get_response_json( + get_json_message( + "Generating top tracks playlist for artist " + + artist_name, + True), + 200) + return get_response_json( + get_json_message( + "Bad request", + False), + 400) + except SubsonicOfflineException: + utils.write_exception() + return get_response_json( + get_json_message( + "Unable to communicate with Subsonic.", + False), + 400) + except SpotifyApiException: + utils.write_exception() + return get_response_json( + get_json_message( + "Spotify API Error. Please check your configuruation.", + False), + 400) + + +@nsgenerate.route('/artist_top_tracks/all/') +class ArtistTopTracksAllClass(Resource): + """All Artists top tracks class""" + + def get(self): + """All Artists top tracks endpoint""" + try: + get_spotipy_helper().get_secrets() + get_subsonic_helper().check_pysonic_connection() + threading.Thread( + target=lambda: generator + .all_artists_top_tracks(get_subsonic_helper() + .get_artists_array_names())).start() + return get_response_json( + get_json_message( + "Generating top tracks playlist for all artists", + True), + 200) + except SubsonicOfflineException: + utils.write_exception() + return get_response_json( + get_json_message( + "Unable to communicate with Subsonic.", + False), + 400) + except SpotifyApiException: + utils.write_exception() + return get_response_json( + get_json_message( + "Spotify API Error. Please check your configuruation.", + False), + 400) + + +@nsgenerate.route('/recommendations') +class RecommendationsClass(Resource): + """Recommendations class""" + + def get(self): + """Recommendations endpoint""" + try: + get_spotipy_helper().get_secrets() + get_subsonic_helper().check_pysonic_connection() + threading.Thread( + target=lambda: generator.my_recommendations( + count=random.randrange( + int( + os.environ.get( + constants.NUM_USER_PLAYLISTS, + constants.NUM_USER_PLAYLISTS_DEFAULT_VALUE))))).start() + return get_response_json( + get_json_message( + "Generating a reccommendation playlist", + True), + 200) + except SubsonicOfflineException: + utils.write_exception() + return get_response_json( + get_json_message( + "Unable to communicate with Subsonic.", + False), + 400) + except SpotifyApiException: + utils.write_exception() + return get_response_json( + get_json_message( + "Spotify API Error. Please check your configuruation.", + False), + 400) + + +nsimport = api.namespace('import', 'Generate APIs') + + +@nsimport.route('/user_playlist/') +@nsimport.route('/user_playlist//') +class UserPlaylistsClass(Resource): + """User playlists class""" + + def get(self, playlist_name=None): + """User playlists endpoint""" + try: + get_spotipy_helper().get_secrets() + get_subsonic_helper().check_pysonic_connection() + if playlist_name is None: + count = generator.count_user_playlists(0) + threading.Thread( + target=lambda: generator.get_user_playlists( + random.randrange(count), + single_execution=True)).start() + return get_response_json(get_json_message( + "Importing a random playlist", True), 200) + search_result_name = generator.get_user_playlist_by_name( + playlist_name) + if search_result_name is None: + return get_response_json( + get_json_message( + "Playlist " + + playlist_name + + " not found, maybe a misspelling error?", + True), + 206) + playlist_name = search_result_name + if playlist_name is not None: + threading.Thread(target=lambda: generator.get_user_playlists( + 0, single_execution=False, playlist_name=playlist_name)).start() + return get_response_json( + get_json_message( + "Searching and importing your spotify account for playlist " + + playlist_name, + True), + 200) + return get_response_json( + get_json_message( + "Bad request", + False), + 400) + except SubsonicOfflineException: + utils.write_exception() + return get_response_json( + get_json_message( + "Unable to communicate with Subsonic.", + False), + 400) + except SpotifyApiException: + utils.write_exception() + return get_response_json( + get_json_message( + "Spotify API Error. Please check your configuruation.", + False), + 400) + + +@nsimport.route('/user_playlist/all/') +class UserPlaylistsAllClass(Resource): + """All User playlists class""" + + def get(self): + """All User playlists endpoint""" + try: + get_spotipy_helper().get_secrets() + get_subsonic_helper().check_pysonic_connection() + threading.Thread( + target=lambda: generator.get_user_playlists(0)).start() + return get_response_json( + get_json_message( + "Importing all your spotify playlists", + True), + 200) + except SubsonicOfflineException: + utils.write_exception() + return get_response_json( + get_json_message( + "Unable to communicate with Subsonic.", + False), + 400) + except SpotifyApiException: + utils.write_exception() + return get_response_json( + get_json_message( + "Spotify API Error. Please check your configuruation.", + False), + 400) + + +@nsimport.route('/saved_tracks') +class SavedTracksClass(Resource): + """Saved tracks class""" + + def get(self): + """Saved tracks endpoint""" + try: + get_spotipy_helper().get_secrets() + get_subsonic_helper().check_pysonic_connection() + threading.Thread( + target=lambda: generator + .get_user_saved_tracks(dict({'tracks': []}))).start() + return get_response_json(get_json_message( + "Importing your saved tracks", True), 200) + except SubsonicOfflineException: + utils.write_exception() + return get_response_json( + get_json_message( + "Unable to communicate with Subsonic.", + False), + 400) + except SpotifyApiException: + utils.write_exception() + return get_response_json( + get_json_message( + "Spotify API Error. Please check your configuruation.", + False), + 400) + + +nsdatabase = api.namespace('database', 'Database APIs') + + +@nsdatabase.route('/playlist/unmatched') +class PlaylistUnmatchedClass(Resource): + """Unmatched playlist songs class""" + + def get(self): + """Unmatched playlist songs endpoint""" + try: + get_spotipy_helper().get_secrets() + get_subsonic_helper().check_pysonic_connection() + missing_songs = get_subsonic_helper().get_playlist_songs( + missing_only=True) + return get_response_json(json.dumps(missing_songs), 200) + except SubsonicOfflineException: + utils.write_exception() + return get_response_json( + get_json_message( + "Unable to communicate with Subsonic.", + False), + 400) + except SpotifyApiException: + utils.write_exception() + return get_response_json( + get_json_message( + "Spotify API Error. Please check your configuruation.", + False), + 400) + + +nsdatabase = api.namespace('database', 'Database APIs') + + +@nsdatabase.route('/playlist/all') +class PlaylistAllClass(Resource): + """All playlist songs class""" + + def get(self): + """All playlist songs endpoint""" + try: + get_spotipy_helper().get_secrets() + get_subsonic_helper().check_pysonic_connection() + missing_songs = get_subsonic_helper().get_playlist_songs( + missing_only=False) + return get_response_json(json.dumps(missing_songs), 200) + except SubsonicOfflineException: + utils.write_exception() + return get_response_json( + get_json_message( + "Unable to communicate with Subsonic.", + False), + 400) + except SpotifyApiException: + utils.write_exception() + return get_response_json( + get_json_message( + "Spotify API Error. Please check your configuruation.", + False), + 400) + + +nsutils = api.namespace('utils', 'Utils APIs') + + +@nsutils.route('/healthcheck') +class Healthcheck(Resource): + """Healthcheck class""" + + def get(self): + """Healthcheck endpoint""" + return "Ok!" + + +scheduler = APScheduler() + +if os.environ.get(constants.SCHEDULER_ENABLED, + constants.SCHEDULER_ENABLED_DEFAULT_VALUE) == "1": + + if os.environ.get(constants.ARTIST_GEN_SCHED, + constants.ARTIST_GEN_SCHED_DEFAULT_VALUE) != "0": + @scheduler.task('interval', + id='artist_recommendations', + hours=int(os.environ.get(constants.ARTIST_GEN_SCHED, + constants.ARTIST_GEN_SCHED_DEFAULT_VALUE))) + def artist_recommendations(): + """artist_recommendations task""" + generator.show_recommendations_for_artist( + random.choice(get_subsonic_helper().get_artists_array_names())) + + if os.environ.get(constants.ARTIST_TOP_GEN_SCHED, + constants.ARTIST_TOP_GEN_SCHED_DEFAULT_VALUE) != "0": + @scheduler.task('interval', + id='artist_top_tracks', + hours=int(os.environ.get(constants.ARTIST_TOP_GEN_SCHED, + constants.ARTIST_TOP_GEN_SCHED_DEFAULT_VALUE))) + def artist_top_tracks(): + """artist_top_tracks task""" + generator.artist_top_tracks( + random.choice(get_subsonic_helper().get_artists_array_names())) + + if os.environ.get(constants.RECOMEND_GEN_SCHED, + constants.RECOMEND_GEN_SCHED_DEFAULT_VALUE) != "0": + @scheduler.task('interval', + id='my_recommendations', + hours=int(os.environ.get(constants.RECOMEND_GEN_SCHED, + constants.RECOMEND_GEN_SCHED_DEFAULT_VALUE))) + def my_recommendations(): + """my_recommendations task""" + generator.my_recommendations(count=random.randrange(int(os.environ.get( + constants.NUM_USER_PLAYLISTS, constants.NUM_USER_PLAYLISTS_DEFAULT_VALUE)))) + + if os.environ.get(constants.PLAYLIST_GEN_SCHED, + constants.PLAYLIST_GEN_SCHED_DEFAULT_VALUE) != "0": + @scheduler.task('interval', + id='user_playlists', + hours=int(os.environ.get(constants.PLAYLIST_GEN_SCHED, + constants.PLAYLIST_GEN_SCHED_DEFAULT_VALUE))) + def user_playlists(): + """user_playlists task""" + generator.get_user_playlists( + random.randrange( + generator.count_user_playlists(0)), + single_execution=True) + + if os.environ.get(constants.SAVED_GEN_SCHED, + constants.SAVED_GEN_SCHED_DEFAULT_VALUE) != "0": + @scheduler.task('interval', + id='saved_tracks', + hours=int(os.environ.get(constants.SAVED_GEN_SCHED, + constants.SAVED_GEN_SCHED_DEFAULT_VALUE))) + def saved_tracks(): + """saved_tracks task""" + generator.get_user_saved_tracks(dict({'tracks': []})) + + +@scheduler.task('interval', id='remove_subsonic_deleted_playlist', hours=12) +def remove_subsonic_deleted_playlist(): + """remove_subsonic_deleted_playlist task""" + get_subsonic_helper().remove_subsonic_deleted_playlist() + + +scheduler.init_app(spotisub) +scheduler.start() diff --git a/spotisub/core/external/utils/utils.py b/spotisub/utils.py similarity index 95% rename from spotisub/core/external/utils/utils.py rename to spotisub/utils.py index c3c8074..251d075 100644 --- a/spotisub/core/external/utils/utils.py +++ b/spotisub/utils.py @@ -3,13 +3,7 @@ import re import sys import logging -from os.path import dirname -from os.path import join -from dotenv import load_dotenv -from .constants import constants - -dotenv_path = join(dirname(__file__), '.env') -load_dotenv(dotenv_path) +from spotisub import constants def print_logo(version): diff --git a/uwsgi.ini b/uwsgi.ini index 3a39b95..058284c 100644 --- a/uwsgi.ini +++ b/uwsgi.ini @@ -4,7 +4,7 @@ uid = root gid = root master = true processes = 1 -threads = 1 +threads = 4 enable-threads = true harakiri = 900 http-timeout = 900 From f69d154795150403af1671c9e83bf9b9b276ed7a Mon Sep 17 00:00:00 2001 From: blast Date: Sun, 22 Sep 2024 02:08:28 +0200 Subject: [PATCH 03/36] dash version --- spotisub/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spotisub/constants.py b/spotisub/constants.py index da31136..9543b91 100644 --- a/spotisub/constants.py +++ b/spotisub/constants.py @@ -1,5 +1,5 @@ """Spotisub constants""" -VERSION = "0.3.0-alpha" +VERSION = "0.3.0-dashboard-alpha" SPLIT_TOKENS = ["(", "-", "feat"] ARTIST_GEN_SCHED = "ARTIST_GEN_SCHED" From e275fd74df2338897fc6fecebffcd8ee9cae57c2 Mon Sep 17 00:00:00 2001 From: blast Date: Sun, 22 Sep 2024 02:17:55 +0200 Subject: [PATCH 04/36] bugfix == None on db --- spotisub/database.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spotisub/database.py b/spotisub/database.py index bf60204..271d1eb 100644 --- a/spotisub/database.py +++ b/spotisub/database.py @@ -211,8 +211,8 @@ def select_all_playlists(self, missing_only): self.subsonic_spotify_relation.c.subsonic_artist_id, self.subsonic_spotify_relation.c.subsonic_playlist_id, self.subsonic_spotify_relation.c.spotify_song_uuid).where( - self.subsonic_spotify_relation.c.subsonic_song_id is None, - self.subsonic_spotify_relation.c.subsonic_artist_id is None) + self.subsonic_spotify_relation.c.subsonic_song_id == None, + self.subsonic_spotify_relation.c.subsonic_artist_id == None) else: stmt = select( self.subsonic_spotify_relation.c.uuid, From 78f14ee3153dba0232180b72fd984edf01a32237 Mon Sep 17 00:00:00 2001 From: blast Date: Sun, 22 Sep 2024 14:18:50 +0200 Subject: [PATCH 05/36] implementing dashboard --- Dockerfile | 8 +- config.py | 12 ++ main.py | 9 +- requirements.txt | 9 +- spotisub/__init__.py | 12 +- spotisub/classes.py | 25 +++ spotisub/database.py | 168 +++++++++--------- spotisub/generator.py | 13 +- spotisub/helpers/lidarr_helper.py | 1 + spotisub/helpers/musicbrainz_helper.py | 1 + spotisub/helpers/spotdl_helper.py | 1 + spotisub/helpers/spotipy_helper.py | 1 + spotisub/helpers/subsonic_helper.py | 29 ++- spotisub/routes.py | 62 +++---- .../templates}/dashboard.html | 0 uwsgi.ini | 6 +- 16 files changed, 202 insertions(+), 155 deletions(-) create mode 100644 config.py rename {templates => spotisub/templates}/dashboard.html (100%) diff --git a/Dockerfile b/Dockerfile index abc460c..70adff5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,10 +20,10 @@ ENV PATH="$HOME/.local/bin:$PATH" WORKDIR $HOME/spotisub ENV PATH="/home/uwsgi/.local/bin:${PATH}" -COPY main.py init.py entrypoint.sh first_run.sh uwsgi.ini requirements.txt ./ -COPY spotisub spotisub/ -COPY templates templates/ +COPY requirements.txt ./ RUN pip3 install --no-cache-dir -r requirements.txt +COPY main.py init.py entrypoint.sh first_run.sh uwsgi.ini ./ +COPY spotisub spotisub/ USER root RUN chmod +x entrypoint.sh && \ @@ -31,4 +31,4 @@ RUN chmod +x entrypoint.sh && \ chown -R user:user . # CMD runs as root initially but switches to the user inside entrypoint.sh -ENTRYPOINT ["./entrypoint.sh"] \ No newline at end of file +ENTRYPOINT ["./entrypoint.sh"] diff --git a/config.py b/config.py new file mode 100644 index 0000000..07830e8 --- /dev/null +++ b/config.py @@ -0,0 +1,12 @@ +import os + +basedir = os.path.abspath(os.path.dirname(__file__)) + + +class Config(object): + """All application configurations""" + + # Database configurations + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ + 'sqlite:///' + os.path.join(basedir, 'cache/configration.db') + SQLALCHEMY_TRACK_MODIFICATIONS = False diff --git a/main.py b/main.py index 4e12b96..b3123b3 100644 --- a/main.py +++ b/main.py @@ -6,4 +6,11 @@ dotenv_path = join(dirname(__file__), '.env') load_dotenv(dotenv_path) -import spotisub \ No newline at end of file +from spotisub import spotisub +from spotisub.models import User +from spotisub import configuration_db + + +@spotisub.shell_context_processor +def make_shell_context(): + return dict(db=configuration_db, User=User) diff --git a/requirements.txt b/requirements.txt index a7827be..65a7476 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,13 @@ spotipy python-dotenv==1.0.0 -werkzeug==2.1.2 -Flask~=2.1.2 -flask-restx==0.5.1 +werkzeug==2.2.2 +Flask~=2.2.2 +flask-restx==1.3.0 click<8.1.0 uWSGI==2.0.21 +Flask-Login==0.6.2 +Flask-SQLAlchemy==3.0.2 +Flask-Bootstrap==3.3.7.1 Flask-APScheduler==1.12.4 py-sonic ffmpeg diff --git a/spotisub/__init__.py b/spotisub/__init__.py index 6722d71..6d9046b 100644 --- a/spotisub/__init__.py +++ b/spotisub/__init__.py @@ -3,6 +3,11 @@ import os from flask import Flask +from flask_bootstrap import Bootstrap +from flask_sqlalchemy import SQLAlchemy +from flask_login import LoginManager +from config import Config +from spotisub import database from spotisub import constants from spotisub import utils @@ -20,7 +25,12 @@ log.setLevel(int(os.environ.get(constants.LOG_LEVEL, constants.LOG_LEVEL_DEFAULT_VALUE))) - spotisub = Flask(__name__) +spotisub.config.from_object(Config) + +bootstrap = Bootstrap(spotisub) +configuration_db = SQLAlchemy(spotisub) +login = LoginManager(spotisub) +login.login_view = 'login' from spotisub import routes \ No newline at end of file diff --git a/spotisub/classes.py b/spotisub/classes.py index 142016c..67fdd78 100644 --- a/spotisub/classes.py +++ b/spotisub/classes.py @@ -1,5 +1,8 @@ """Spotisub classes""" +from spotisub import configuration_db, login +from flask_login import UserMixin +from werkzeug.security import generate_password_hash, check_password_hash class ComparisonHelper: def __init__(self, track, artist_spotify, found, @@ -10,3 +13,25 @@ def __init__(self, track, artist_spotify, found, self.excluded = excluded self.song_ids = song_ids self.track_helper = track_helper + +@login.user_loader +def load_user(id): + """Load user by their ID""" + return User.query.get(int(id)) + +class User(configuration_db.Model, UserMixin): + """User table""" + id = configuration_db.Column(configuration_db.Integer, primary_key=True) + username = configuration_db.Column(configuration_db.String(32), unique=True, index=True) + password_hash = configuration_db.Column(configuration_db.String(128)) + + def __repr__(self): + return f'User: {self.username}' + + def set_password(self, password): + """Hash user password befor storage""" + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + """Confirms a user's password""" + return check_password_hash(self.password_hash, password) \ No newline at end of file diff --git a/spotisub/database.py b/spotisub/database.py index bf60204..f89942e 100644 --- a/spotisub/database.py +++ b/spotisub/database.py @@ -12,7 +12,6 @@ from sqlalchemy.sql import func SQLITE = 'sqlite' -USER = 'user' SUBSONIC_PLAYLIST = 'subsonic_playlist' SUBSONIC_SONG = 'subsonic_song' SUBSONIC_ARTIST = 'subsonic_artist' @@ -40,15 +39,6 @@ def __init__(self, dbtype, dbname=''): metadata = MetaData() - user = Table(USER, metadata, - Column( - 'id', String(36), primary_key=True, nullable=False), - Column( - 'username', String(36), unique=True, index=True, nullable=False), - Column( - 'password_hash', String(128), nullable=False) - ) - subsonic_spotify_relation = Table(SUBSONIC_SPOTIFY_RELATION, metadata, Column( 'uuid', String(36), primary_key=True, nullable=False), @@ -116,21 +106,21 @@ def __init__(self, dbtype, dbname=''): ) -def create_db_tables(self): +def create_db_tables(): """Create tables""" - self.metadata.create_all(self.db_engine) + dbms.metadata.create_all(dbms.db_engine) -def insert_song(self, playlist_id, subsonic_track, +def insert_song(playlist_id, subsonic_track, artist_spotify, track_spotify): """insert song into database""" - with self.db_engine.connect() as conn: + with dbms.db_engine.connect() as conn: spotify_song_uuid = insert_spotify_song( - self, conn, artist_spotify, track_spotify) + conn, artist_spotify, track_spotify) if spotify_song_uuid is not None: if subsonic_track is None: insert_playlist_relation( - self, conn, None, None, playlist_id, spotify_song_uuid) + conn, None, None, playlist_id, spotify_song_uuid) else: track_id = None artist_id = None @@ -139,7 +129,6 @@ def insert_song(self, playlist_id, subsonic_track, if "artistId" in subsonic_track: artist_id = subsonic_track["artistId"] insert_playlist_relation( - self, conn, track_id, artist_id, @@ -151,44 +140,44 @@ def insert_song(self, playlist_id, subsonic_track, conn.close() -def delete_playlist_relation_by_id(self, playlist_id: str): +def delete_playlist_relation_by_id(playlist_id: str): """delete playlist from database""" - stmt = delete(self.subsonic_spotify_relation).where( - self.subsonic_spotify_relation.c.subsonic_playlist_id == playlist_id) + stmt = delete(dbms.subsonic_spotify_relation).where( + dbms.subsonic_spotify_relation.c.subsonic_playlist_id == playlist_id) stmt.compile() - with self.db_engine.connect() as conn: + with dbms.db_engine.connect() as conn: conn.execute(stmt) conn.commit() conn.close() -def delete_song_relation(self, playlist_id: str, subsonic_track): +def delete_song_relation(playlist_id: str, subsonic_track): """delete playlist from database""" if "id" in subsonic_track: stmt = None if "artistId" in subsonic_track: - stmt = delete(self.subsonic_spotify_relation).where( - self.subsonic_spotify_relation.c.subsonic_playlist_id == playlist_id, - self.subsonic_spotify_relation.c.subsonic_song_id == subsonic_track["id"], - self.subsonic_spotify_relation.c.subsonic_artist_id == subsonic_track["artistId"]) + stmt = delete(dbms.subsonic_spotify_relation).where( + dbms.subsonic_spotify_relation.c.subsonic_playlist_id == playlist_id, + dbms.subsonic_spotify_relation.c.subsonic_song_id == subsonic_track["id"], + dbms.subsonic_spotify_relation.c.subsonic_artist_id == subsonic_track["artistId"]) else: - stmt = delete(self.subsonic_spotify_relation).where( - self.subsonic_spotify_relation.c.subsonic_playlist_id == playlist_id, - self.subsonic_spotify_relation.c.subsonic_song_id == subsonic_track["id"]) + stmt = delete(dbms.subsonic_spotify_relation).where( + dbms.subsonic_spotify_relation.c.subsonic_playlist_id == playlist_id, + dbms.subsonic_spotify_relation.c.subsonic_song_id == subsonic_track["id"]) stmt.compile() - with self.db_engine.connect() as conn: + with dbms.db_engine.connect() as conn: conn.execute(stmt) conn.commit() conn.close() -def insert_playlist_relation(self, conn, subsonic_song_id, +def insert_playlist_relation(conn, subsonic_song_id, subsonic_artist_id, subsonic_playlist_id, spotify_song_uuid): """insert playlist into database""" stmt = insert( - self.subsonic_spotify_relation).values( + dbms.subsonic_spotify_relation).values( uuid=str( uuid.uuid4().hex), subsonic_song_id=subsonic_song_id, @@ -199,41 +188,41 @@ def insert_playlist_relation(self, conn, subsonic_song_id, conn.execute(stmt) -def select_all_playlists(self, missing_only): +def select_all_playlists(missing_only): """select playlists from database""" value = {} stmt = None - with self.db_engine.connect() as conn: + with dbms.db_engine.connect() as conn: if missing_only: stmt = select( - self.subsonic_spotify_relation.c.uuid, - self.subsonic_spotify_relation.c.subsonic_song_id, - self.subsonic_spotify_relation.c.subsonic_artist_id, - self.subsonic_spotify_relation.c.subsonic_playlist_id, - self.subsonic_spotify_relation.c.spotify_song_uuid).where( - self.subsonic_spotify_relation.c.subsonic_song_id is None, - self.subsonic_spotify_relation.c.subsonic_artist_id is None) + dbms.subsonic_spotify_relation.c.uuid, + dbms.subsonic_spotify_relation.c.subsonic_song_id, + dbms.subsonic_spotify_relation.c.subsonic_artist_id, + dbms.subsonic_spotify_relation.c.subsonic_playlist_id, + dbms.subsonic_spotify_relation.c.spotify_song_uuid).where( + dbms.subsonic_spotify_relation.c.subsonic_song_id is None, + dbms.subsonic_spotify_relation.c.subsonic_artist_id is None) else: stmt = select( - self.subsonic_spotify_relation.c.uuid, - self.subsonic_spotify_relation.c.subsonic_song_id, - self.subsonic_spotify_relation.c.subsonic_artist_id, - self.subsonic_spotify_relation.c.subsonic_playlist_id, - self.subsonic_spotify_relation.c.spotify_song_uuid) + dbms.subsonic_spotify_relation.c.uuid, + dbms.subsonic_spotify_relation.c.subsonic_song_id, + dbms.subsonic_spotify_relation.c.subsonic_artist_id, + dbms.subsonic_spotify_relation.c.subsonic_playlist_id, + dbms.subsonic_spotify_relation.c.spotify_song_uuid) stmt.compile() cursor = conn.execute(stmt) records = cursor.fetchall() for row in records: song = select_spotify_song_by_uuid( - self, conn, row.spotify_song_uuid) + conn, row.spotify_song_uuid) if song is not None and song.title is not None: artists_relation = select_spotify_song_artists_relation_by_song_uuid( - self, conn, row.spotify_song_uuid) + conn, row.spotify_song_uuid) artists = [] for artist_rel in artists_relation: artist_found = select_spotify_artists_by_uuid( - self, conn, artist_rel.artist_uuid) + conn, artist_rel.artist_uuid) if artist_found is not None and artist_found.name is not None: artist_row = {} artist_row["name"] = artist_found.name @@ -258,14 +247,14 @@ def select_all_playlists(self, missing_only): return value -def select_spotify_artists_by_uuid(self, conn, c_uuid): +def select_spotify_artists_by_uuid(conn, c_uuid): """select spotify artists by uuid""" value = None stmt = select( - self.spotify_artist.c.uuid, - self.spotify_artist.c.name, - self.spotify_artist.c.spotify_uri).where( - self.spotify_artist.c.uuid == c_uuid) + dbms.spotify_artist.c.uuid, + dbms.spotify_artist.c.name, + dbms.spotify_artist.c.spotify_uri).where( + dbms.spotify_artist.c.uuid == c_uuid) stmt.compile() cursor = conn.execute(stmt) records = cursor.fetchall() @@ -277,14 +266,14 @@ def select_spotify_artists_by_uuid(self, conn, c_uuid): return value -def insert_spotify_song(self, conn, artist_spotify, track_spotify): +def insert_spotify_song(conn, artist_spotify, track_spotify): """insert spotify song""" - song_db = select_spotify_song_by_uri(self, conn, track_spotify["uri"]) + song_db = select_spotify_song_by_uri(conn, track_spotify["uri"]) song_uuid = None if song_db is None: song_uuid = str(uuid.uuid4().hex) stmt = insert( - self.spotify_song).values( + dbms.spotify_song).values( uuid=song_uuid, title=track_spotify["name"], spotify_uri=track_spotify["uri"]) @@ -294,22 +283,22 @@ def insert_spotify_song(self, conn, artist_spotify, track_spotify): song_uuid = song_db.uuid if song_uuid is not None: - artist_uuid = insert_spotify_artist(self, conn, artist_spotify) + artist_uuid = insert_spotify_artist(conn, artist_spotify) if artist_uuid is not None: insert_spotify_song_artist_relation( - self, conn, song_uuid, artist_uuid) + conn, song_uuid, artist_uuid) return song_uuid return song_uuid -def select_spotify_song_by_uri(self, conn, spotify_uri: str): +def select_spotify_song_by_uri(conn, spotify_uri: str): """select spotify song by uri""" value = None stmt = select( - self.spotify_song.c.uuid, - self.spotify_song.c.spotify_uri, - self.spotify_song.c.title).where( - self.spotify_song.c.spotify_uri == spotify_uri) + dbms.spotify_song.c.uuid, + dbms.spotify_song.c.spotify_uri, + dbms.spotify_song.c.title).where( + dbms.spotify_song.c.spotify_uri == spotify_uri) stmt.compile() cursor = conn.execute(stmt) records = cursor.fetchall() @@ -321,14 +310,14 @@ def select_spotify_song_by_uri(self, conn, spotify_uri: str): return value -def select_spotify_song_by_uuid(self, conn, c_uuid): +def select_spotify_song_by_uuid(conn, c_uuid): """select spotify song by uuid""" value = None stmt = select( - self.spotify_song.c.uuid, - self.spotify_song.c.spotify_uri, - self.spotify_song.c.title).where( - self.spotify_song.c.uuid == c_uuid) + dbms.spotify_song.c.uuid, + dbms.spotify_song.c.spotify_uri, + dbms.spotify_song.c.title).where( + dbms.spotify_song.c.uuid == c_uuid) stmt.compile() cursor = conn.execute(stmt) records = cursor.fetchall() @@ -340,13 +329,13 @@ def select_spotify_song_by_uuid(self, conn, c_uuid): return value -def insert_spotify_artist(self, conn, artist_spotify): +def insert_spotify_artist(conn, artist_spotify): """insert spotify artist""" - artist_db = select_spotify_artist_by_uri(self, conn, artist_spotify["uri"]) + artist_db = select_spotify_artist_by_uri(conn, artist_spotify["uri"]) if artist_db is None: artist_uuid = str(uuid.uuid4().hex) stmt = insert( - self.spotify_artist).values( + dbms.spotify_artist).values( uuid=artist_uuid, name=artist_spotify["name"], spotify_uri=artist_spotify["uri"]) @@ -358,14 +347,14 @@ def insert_spotify_artist(self, conn, artist_spotify): return None -def select_spotify_artist_by_uri(self, conn, spotify_uri: str): +def select_spotify_artist_by_uri(conn, spotify_uri: str): """select spotify artist by uri""" value = None stmt = select( - self.spotify_artist.c.uuid, - self.spotify_artist.c.name, - self.spotify_artist.c.spotify_uri).where( - self.spotify_artist.c.spotify_uri == spotify_uri) + dbms.spotify_artist.c.uuid, + dbms.spotify_artist.c.name, + dbms.spotify_artist.c.spotify_uri).where( + dbms.spotify_artist.c.spotify_uri == spotify_uri) stmt.compile() cursor = conn.execute(stmt) records = cursor.fetchall() @@ -378,12 +367,12 @@ def select_spotify_artist_by_uri(self, conn, spotify_uri: str): def insert_spotify_song_artist_relation( - self, conn, song_uuid: int, artist_uuid: int): + conn, song_uuid: int, artist_uuid: int): """insert spotify song artist relation""" if select_spotify_song_artist_relation( - self, conn, song_uuid, artist_uuid) is None: + conn, song_uuid, artist_uuid) is None: stmt = insert( - self.spotify_song_artist_relation).values( + dbms.spotify_song_artist_relation).values( song_uuid=song_uuid, artist_uuid=artist_uuid) stmt.compile() @@ -392,14 +381,14 @@ def insert_spotify_song_artist_relation( def select_spotify_song_artist_relation( - self, conn, song_uuid: int, artist_uuid: int): + conn, song_uuid: int, artist_uuid: int): """select spotify song artist relation""" value = None stmt = select( - self.spotify_song_artist_relation.c.song_uuid, - self.spotify_song_artist_relation.c.artist_uuid).where( - self.spotify_song_artist_relation.c.song_uuid == song_uuid, - self.spotify_song_artist_relation.c.artist_uuid == artist_uuid) + dbms.spotify_song_artist_relation.c.song_uuid, + dbms.spotify_song_artist_relation.c.artist_uuid).where( + dbms.spotify_song_artist_relation.c.song_uuid == song_uuid, + dbms.spotify_song_artist_relation.c.artist_uuid == artist_uuid) stmt.compile() cursor = conn.execute(stmt) records = cursor.fetchall() @@ -412,11 +401,11 @@ def select_spotify_song_artist_relation( def select_spotify_song_artists_relation_by_song_uuid( - self, conn, song_uuid: int): + conn, song_uuid: int): """select spotify song artist relation by song uuid""" value = [] - stmt = select(self.spotify_song_artist_relation.c.artist_uuid).where( - self.spotify_song_artist_relation.c.song_uuid == song_uuid) + stmt = select(dbms.spotify_song_artist_relation.c.artist_uuid).where( + dbms.spotify_song_artist_relation.c.song_uuid == song_uuid) stmt.compile() cursor = conn.execute(stmt) records = cursor.fetchall() @@ -426,3 +415,6 @@ def select_spotify_song_artists_relation_by_song_uuid( cursor.close() return value + +dbms = Database(SQLITE, dbname='spotisub.sqlite3') +create_db_tables() \ No newline at end of file diff --git a/spotisub/generator.py b/spotisub/generator.py index 7368771..8d7e548 100644 --- a/spotisub/generator.py +++ b/spotisub/generator.py @@ -4,19 +4,14 @@ import random import time import re +from spotisub import spotisub from spotisub import constants from spotisub.helpers import spotipy_helper from spotisub.helpers import subsonic_helper - -def get_spotipy_helper(): - """get spotipy helper""" - return spotipy_helper - - -def get_subsonic_helper(): - """get subsonic helper""" - return subsonic_helper +def prechecks(): + spotipy_helper.get_secrets() + subsonic_helper.check_pysonic_connection() def artist_top_tracks(query): diff --git a/spotisub/helpers/lidarr_helper.py b/spotisub/helpers/lidarr_helper.py index 23b89c9..89c9fd7 100644 --- a/spotisub/helpers/lidarr_helper.py +++ b/spotisub/helpers/lidarr_helper.py @@ -3,6 +3,7 @@ from pyarr import LidarrAPI from expiringdict import ExpiringDict +from spotisub import spotisub from spotisub import utils from spotisub import constants diff --git a/spotisub/helpers/musicbrainz_helper.py b/spotisub/helpers/musicbrainz_helper.py index 2e8205c..027b1cc 100644 --- a/spotisub/helpers/musicbrainz_helper.py +++ b/spotisub/helpers/musicbrainz_helper.py @@ -4,6 +4,7 @@ import musicbrainzngs +from spotisub import spotisub from spotisub import utils diff --git a/spotisub/helpers/spotdl_helper.py b/spotisub/helpers/spotdl_helper.py index c8767b9..f3cb4e7 100644 --- a/spotisub/helpers/spotdl_helper.py +++ b/spotisub/helpers/spotdl_helper.py @@ -3,6 +3,7 @@ from spotdl import Spotdl from spotdl.types.song import Song +from spotisub import spotisub from spotisub import constants diff --git a/spotisub/helpers/spotipy_helper.py b/spotisub/helpers/spotipy_helper.py index 48e50e7..f6aa9c5 100644 --- a/spotisub/helpers/spotipy_helper.py +++ b/spotisub/helpers/spotipy_helper.py @@ -2,6 +2,7 @@ import os import spotipy from spotipy import SpotifyOAuth +from spotisub import spotisub from spotisub import constants from spotisub.exceptions import SpotifyApiException diff --git a/spotisub/helpers/subsonic_helper.py b/spotisub/helpers/subsonic_helper.py index 4085216..6600b92 100644 --- a/spotisub/helpers/subsonic_helper.py +++ b/spotisub/helpers/subsonic_helper.py @@ -5,10 +5,11 @@ import time import libsonic from libsonic.errors import DataNotFoundError +from spotisub import spotisub +from spotisub import database from spotisub import constants from spotisub import utils from spotisub.exceptions import SubsonicOfflineException -from spotisub import database from spotisub.classes import ComparisonHelper from spotisub.helpers import musicbrainz_helper @@ -30,10 +31,6 @@ "if an artist won't be found inside the " + "lidarr database, the download process will be skipped.") - -dbms = database.Database(database.SQLITE, dbname='spotisub.sqlite3') -database.create_db_tables(dbms) - pysonic = libsonic.Connection( os.environ.get( constants.SUBSONIC_API_HOST), @@ -155,7 +152,7 @@ def write_playlist(sp, playlist_name, results): check_pysonic_connection().createPlaylist(name=playlist_name, songIds=[]) logging.info('Creating playlist %s', playlist_name) playlist_id = get_playlist_id_by_name(playlist_name) - database.delete_playlist_relation_by_id(dbms, playlist_id) + database.delete_playlist_relation_by_id(playlist_id) else: old_song_ids = get_playlist_songs_ids_by_id(playlist_id) @@ -226,7 +223,7 @@ def write_playlist(sp, playlist_name, results): artist_spotify["name"], track['name']) database.insert_song( - dbms, playlist_id, None, artist_spotify, track) + playlist_id, None, artist_spotify, track) if playlist_id is not None: if len(song_ids) > 0: @@ -296,7 +293,7 @@ def match_with_subsonic_track( comparison_helper.track_helper.append(placeholder) comparison_helper.found = True database.insert_song( - dbms, playlist_id, song, comparison_helper.artist_spotify, comparison_helper.track) + playlist_id, song, comparison_helper.artist_spotify, comparison_helper.track) logging.info( 'Adding song "%s - %s - %s" to playlist "%s", matched by ISRC: "%s"', song["artist"], @@ -323,7 +320,7 @@ def match_with_subsonic_track( comparison_helper.track_helper.append(placeholder) comparison_helper.found = True database.insert_song( - dbms, playlist_id, song, comparison_helper.artist_spotify, comparison_helper.track) + playlist_id, song, comparison_helper.artist_spotify, comparison_helper.track) logging.info( 'Adding song "%s - %s - %s" to playlist "%s", matched by text comparison', song["artist"], @@ -345,7 +342,7 @@ def match_with_subsonic_track( comparison_helper.song_ids.append(skipped_song["id"]) comparison_helper.found = True database.insert_song( - dbms, playlist_id, skipped_song, comparison_helper.artist_spotify, comparison_helper.track) + playlist_id, skipped_song, comparison_helper.artist_spotify, comparison_helper.track) logging.warning( 'No matching album found for Subsonic search "%s", using a random one', text_to_search) @@ -362,7 +359,7 @@ def match_with_subsonic_track( def get_playlist_songs(missing_only=False): """get list of playlists and songs""" - playlist_songs_db = database.select_all_playlists(dbms, missing_only) + playlist_songs_db = database.select_all_playlists(missing_only) playlist_songs = {} for key in playlist_songs_db: playlist_search = None @@ -378,7 +375,7 @@ def get_playlist_songs(missing_only=False): key) logging.warning( 'Deleting Playlist with id "%s" from spotisub database.', key) - database.delete_playlist_relation_by_id(dbms, key) + database.delete_playlist_relation_by_id(key) elif playlist_search is not None: missings = playlist_songs_db[key] for missing in missings: @@ -419,7 +416,7 @@ def get_playlist_songs(missing_only=False): 'with an unmatched Subsonic entry. ' + 'Deleting this playlist.', single_playlist_search["name"]) - database.delete_playlist_relation_by_id(dbms, key) + database.delete_playlist_relation_by_id(key) if single_playlist_search["name"] in playlist_songs: playlist_songs.pop( single_playlist_search["name"]) @@ -450,7 +447,7 @@ def get_playlist_songs_ids_by_id(key): key) logging.warning( 'Deleting Playlist with id "%s" from spotisub database.', key) - database.delete_playlist_relation_by_id(dbms, key) + database.delete_playlist_relation_by_id(key) elif (playlist_search is not None and "playlist" in playlist_search and "entry" in playlist_search["playlist"] @@ -467,7 +464,7 @@ def get_playlist_songs_ids_by_id(key): def remove_subsonic_deleted_playlist(): """fix user manually deleted playlists""" - spotisub_playlists = database.select_all_playlists(dbms, False) + spotisub_playlists = database.select_all_playlists(False) for key in spotisub_playlists: playlist_search = None try: @@ -483,7 +480,7 @@ def remove_subsonic_deleted_playlist(): key) logging.warning( 'Deleting Playlist with id "%s" from spotisub database.', key) - database.delete_playlist_relation_by_id(dbms, key) + database.delete_playlist_relation_by_id(key) # DO we really need to remove spotify songs even if they are not related to any playlist? # This can cause errors when an import process is running diff --git a/spotisub/routes.py b/spotisub/routes.py index c5c0572..6af2cf4 100644 --- a/spotisub/routes.py +++ b/spotisub/routes.py @@ -15,8 +15,8 @@ from spotisub import utils from spotisub import constants from spotisub import generator -from spotisub.generator import get_subsonic_helper -from spotisub.generator import get_spotipy_helper +from spotisub.generator import subsonic_helper +from spotisub.generator import spotipy_helper from spotisub.exceptions import SubsonicOfflineException from spotisub.exceptions import SpotifyApiException @@ -72,13 +72,13 @@ class ArtistRecommendationsClass(Resource): def get(self, artist_name=None): """Artist reccomendations endpoint""" try: - get_spotipy_helper().get_secrets() - get_subsonic_helper().check_pysonic_connection() + spotipy_helper.get_secrets() + subsonic_helper.check_pysonic_connection() if artist_name is None: artist_name = random.choice( - get_subsonic_helper().get_artists_array_names()) + subsonic_helper.get_artists_array_names()) else: - search_result_name = get_subsonic_helper().search_artist(artist_name) + search_result_name = subsonic_helper.search_artist(artist_name) if search_result_name is None: return get_response_json( get_json_message( @@ -126,8 +126,8 @@ class ArtistRecommendationsAllClass(Resource): def get(self): """All Artists reccomendations endpoint""" try: - get_spotipy_helper().get_secrets() - get_subsonic_helper().check_pysonic_connection() + spotipy_helper.get_secrets() + subsonic_helper.check_pysonic_connection() threading.Thread( target=lambda: generator .all_artists_recommendations(get_subsonic_helper() @@ -161,13 +161,13 @@ class ArtistTopTracksClass(Resource): def get(self, artist_name=None): """Artist top tracks endpoint""" try: - get_spotipy_helper().get_secrets() - get_subsonic_helper().check_pysonic_connection() + spotipy_helper.get_secrets() + subsonic_helper.check_pysonic_connection() if artist_name is None: artist_name = random.choice( - get_subsonic_helper().get_artists_array_names()) + subsonic_helper.get_artists_array_names()) else: - search_result_name = get_subsonic_helper().search_artist(artist_name) + search_result_name = subsonic_helper.search_artist(artist_name) if search_result_name is None: return get_response_json( get_json_message( @@ -214,8 +214,8 @@ class ArtistTopTracksAllClass(Resource): def get(self): """All Artists top tracks endpoint""" try: - get_spotipy_helper().get_secrets() - get_subsonic_helper().check_pysonic_connection() + spotipy_helper.get_secrets() + subsonic_helper.check_pysonic_connection() threading.Thread( target=lambda: generator .all_artists_top_tracks(get_subsonic_helper() @@ -248,8 +248,8 @@ class RecommendationsClass(Resource): def get(self): """Recommendations endpoint""" try: - get_spotipy_helper().get_secrets() - get_subsonic_helper().check_pysonic_connection() + spotipy_helper.get_secrets() + subsonic_helper.check_pysonic_connection() threading.Thread( target=lambda: generator.my_recommendations( count=random.randrange( @@ -289,8 +289,8 @@ class UserPlaylistsClass(Resource): def get(self, playlist_name=None): """User playlists endpoint""" try: - get_spotipy_helper().get_secrets() - get_subsonic_helper().check_pysonic_connection() + spotipy_helper.get_secrets() + subsonic_helper.check_pysonic_connection() if playlist_name is None: count = generator.count_user_playlists(0) threading.Thread( @@ -347,8 +347,8 @@ class UserPlaylistsAllClass(Resource): def get(self): """All User playlists endpoint""" try: - get_spotipy_helper().get_secrets() - get_subsonic_helper().check_pysonic_connection() + spotipy_helper.get_secrets() + subsonic_helper.check_pysonic_connection() threading.Thread( target=lambda: generator.get_user_playlists(0)).start() return get_response_json( @@ -379,8 +379,8 @@ class SavedTracksClass(Resource): def get(self): """Saved tracks endpoint""" try: - get_spotipy_helper().get_secrets() - get_subsonic_helper().check_pysonic_connection() + spotipy_helper.get_secrets() + subsonic_helper.check_pysonic_connection() threading.Thread( target=lambda: generator .get_user_saved_tracks(dict({'tracks': []}))).start() @@ -412,9 +412,9 @@ class PlaylistUnmatchedClass(Resource): def get(self): """Unmatched playlist songs endpoint""" try: - get_spotipy_helper().get_secrets() - get_subsonic_helper().check_pysonic_connection() - missing_songs = get_subsonic_helper().get_playlist_songs( + spotipy_helper.get_secrets() + subsonic_helper.check_pysonic_connection() + missing_songs = subsonic_helper.get_playlist_songs( missing_only=True) return get_response_json(json.dumps(missing_songs), 200) except SubsonicOfflineException: @@ -443,9 +443,9 @@ class PlaylistAllClass(Resource): def get(self): """All playlist songs endpoint""" try: - get_spotipy_helper().get_secrets() - get_subsonic_helper().check_pysonic_connection() - missing_songs = get_subsonic_helper().get_playlist_songs( + spotipy_helper.get_secrets() + subsonic_helper.check_pysonic_connection() + missing_songs = subsonic_helper.get_playlist_songs( missing_only=False) return get_response_json(json.dumps(missing_songs), 200) except SubsonicOfflineException: @@ -490,7 +490,7 @@ def get(self): def artist_recommendations(): """artist_recommendations task""" generator.show_recommendations_for_artist( - random.choice(get_subsonic_helper().get_artists_array_names())) + random.choice(subsonic_helper.get_artists_array_names())) if os.environ.get(constants.ARTIST_TOP_GEN_SCHED, constants.ARTIST_TOP_GEN_SCHED_DEFAULT_VALUE) != "0": @@ -501,7 +501,7 @@ def artist_recommendations(): def artist_top_tracks(): """artist_top_tracks task""" generator.artist_top_tracks( - random.choice(get_subsonic_helper().get_artists_array_names())) + random.choice(subsonic_helper.get_artists_array_names())) if os.environ.get(constants.RECOMEND_GEN_SCHED, constants.RECOMEND_GEN_SCHED_DEFAULT_VALUE) != "0": @@ -541,7 +541,7 @@ def saved_tracks(): @scheduler.task('interval', id='remove_subsonic_deleted_playlist', hours=12) def remove_subsonic_deleted_playlist(): """remove_subsonic_deleted_playlist task""" - get_subsonic_helper().remove_subsonic_deleted_playlist() + subsonic_helper.remove_subsonic_deleted_playlist() scheduler.init_app(spotisub) diff --git a/templates/dashboard.html b/spotisub/templates/dashboard.html similarity index 100% rename from templates/dashboard.html rename to spotisub/templates/dashboard.html diff --git a/uwsgi.ini b/uwsgi.ini index 058284c..d5828e4 100644 --- a/uwsgi.ini +++ b/uwsgi.ini @@ -1,5 +1,7 @@ [uwsgi] -module = main:app +spp = spotisub +module = spotisub +callable = spotisub uid = root gid = root master = true @@ -25,4 +27,4 @@ route = ^.*healthcheck.*$ donotlog: log-4xx = true log-5xx = true disable-logging = true - \ No newline at end of file + From 88259cb242db096e69311cdcc21a49062587fe3a Mon Sep 17 00:00:00 2001 From: blast Date: Sun, 22 Sep 2024 14:20:46 +0200 Subject: [PATCH 06/36] adding htmls --- spotisub/static/css/current_user_main.css | 261 ++++++++++++++++++++++ spotisub/static/js/main.js | 17 ++ spotisub/templates/_flash_message.html | 11 + spotisub/templates/base.html | 176 +++++++++++++++ spotisub/templates/dashboard.html | 96 +++++++- spotisub/templates/errors/404.html | 21 ++ spotisub/templates/errors/500.html | 21 ++ spotisub/templates/login.html | 27 +++ spotisub/templates/register.html | 24 ++ 9 files changed, 653 insertions(+), 1 deletion(-) create mode 100644 spotisub/static/css/current_user_main.css create mode 100644 spotisub/static/js/main.js create mode 100644 spotisub/templates/_flash_message.html create mode 100644 spotisub/templates/base.html create mode 100644 spotisub/templates/errors/404.html create mode 100644 spotisub/templates/errors/500.html create mode 100644 spotisub/templates/login.html create mode 100644 spotisub/templates/register.html diff --git a/spotisub/static/css/current_user_main.css b/spotisub/static/css/current_user_main.css new file mode 100644 index 0000000..2f21b1b --- /dev/null +++ b/spotisub/static/css/current_user_main.css @@ -0,0 +1,261 @@ +/* + DEMO STYLE +*/ + +@import "https://fonts.googleapis.com/css?family=Poppins:300,400,500,600,700"; +body { + font-family: 'Poppins', sans-serif; + background: #fafafa; +} + +p { + font-family: 'Poppins', sans-serif; + font-size: 1.1em; + font-weight: 300; + line-height: 1.7em; + color: #999; +} + +a, +a:hover, +a:focus { + color: inherit; + text-decoration: none; + transition: all 0.3s; +} + +.navbar { + padding: 15px 10px; + background: #fff; + border: none; + border-radius: 0; + margin-bottom: 40px; + box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.1); +} + +.navbar-btn { + box-shadow: none; + outline: none !important; + border: none; +} + +.line { + width: 100%; + height: 1px; + border-bottom: 1px dashed #ddd; + margin: 40px 0; +} + +i, +span { + display: inline-block; +} + +/* --------------------------------------------------- + SIDEBAR STYLE +----------------------------------------------------- */ + +.wrapper { + display: flex; + align-items: stretch; +} + +#sidebar { + min-width: 250px; + max-width: 250px; + background: #7386D5; + color: #fff; + transition: all 0.3s; +} + +/* Shrinking the sidebar from 250px to 80px and center aligining its content*/ +#sidebar.active { + min-width: 80px; + max-width: 80px; + text-align: center; +} + +/* Toggling the sidebar header content, hide the big heading [h3] and showing the small heading [strong] and vice versa*/ +#sidebar.active .sidebar-header h3, +#sidebar.active .CTAs { + display: none; +} + +#sidebar.active .sidebar-header strong { + display: block; +} + +#sidebar ul li a { + text-align: left; +} + +#sidebar.active ul li a { + padding: 20px 10px; + text-align: center; + font-size: 0.85em; +} + +#sidebar.active ul li a i { + margin-right: 0; + display: block; + font-size: 1.8em; + margin-bottom: 5px; +} + +/* Same dropdown links padding*/ +#sidebar.active ul ul a { + padding: 10px !important; +} + +/* Changing the arrow position to bottom center position, + translateX(50%) works with right: 50% + to accurately center the arrow */ +#sidebar.active .dropdown-toggle::after { + top: auto; + bottom: 10px; + right: 50%; + -webkit-transform: translateX(50%); + -ms-transform: translateX(50%); + transform: translateX(50%); +} + +#sidebar .sidebar-header { + padding: 20px; + background: #6d7fcc; +} + +#sidebar .sidebar-header strong { + display: none; + font-size: 1.8em; +} + +#sidebar ul.components { + padding: 20px 0; + border-bottom: 1px solid #47748b; +} + +#sidebar ul li a { + padding: 10px; + font-size: 1.1em; + display: block; +} + +#sidebar ul li a:hover { + color: #7386D5; + background: #fff; +} + +#sidebar ul li a i { + margin-right: 10px; +} + +#sidebar ul li.active>a, +a[aria-expanded="true"] { + color: #fff; + background: #6d7fcc; +} + +a[data-toggle="collapse"] { + position: relative; +} + +.dropdown-toggle::after { + display: block; + position: absolute; + top: 50%; + right: 20px; + transform: translateY(-50%); +} + +ul ul a { + font-size: 0.9em !important; + padding-left: 30px !important; + background: #6d7fcc; +} + +ul.CTAs { + padding: 20px; +} + +ul.CTAs a { + text-align: center; + font-size: 0.9em !important; + display: block; + border-radius: 5px; + margin-bottom: 5px; +} + +a.download { + background: #fff; + color: #7386D5; +} + +a.article, +a.article:hover { + background: #6d7fcc !important; + color: #fff !important; +} + +/* --------------------------------------------------- + CONTENT STYLE +----------------------------------------------------- */ + +#content { + width: 100%; + padding: 20px; + min-height: 100vh; + transition: all 0.3s; +} + +/* --------------------------------------------------- + MEDIAQUERIES +----------------------------------------------------- */ + +@media (max-width: 768px) { + #sidebar { + min-width: 80px; + max-width: 80px; + text-align: center; + margin-left: -80px !important; + } + .dropdown-toggle::after { + top: auto; + bottom: 10px; + right: 50%; + -webkit-transform: translateX(50%); + -ms-transform: translateX(50%); + transform: translateX(50%); + } + #sidebar.active { + margin-left: 0 !important; + } + #sidebar .sidebar-header h3, + #sidebar .CTAs { + display: none; + } + #sidebar .sidebar-header strong { + display: block; + } + #sidebar ul li a { + padding: 20px 10px; + } + #sidebar ul li a span { + font-size: 0.85em; + } + #sidebar ul li a i { + margin-right: 0; + display: block; + } + #sidebar ul ul a { + padding: 10px !important; + } + #sidebar ul li a i { + font-size: 1.3em; + } + #sidebar { + margin-left: 0; + } + #sidebarCollapse span { + display: none; + } +} \ No newline at end of file diff --git a/spotisub/static/js/main.js b/spotisub/static/js/main.js new file mode 100644 index 0000000..080e3cc --- /dev/null +++ b/spotisub/static/js/main.js @@ -0,0 +1,17 @@ +$(document).ready(function () { + $("#sidebar").mCustomScrollbar({ + theme: "minimal" + }); + + $('#dismiss, .overlay').on('click', function () { + $('#sidebar').removeClass('active'); + $('.overlay').removeClass('active'); + }); + + $('#sidebarCollapse').on('click', function () { + $('#sidebar').addClass('active'); + $('.overlay').addClass('active'); + $('.collapse.in').toggleClass('in'); + $('a[aria-expanded=true]').attr('aria-expanded', 'false'); + }); +}); \ No newline at end of file diff --git a/spotisub/templates/_flash_message.html b/spotisub/templates/_flash_message.html new file mode 100644 index 0000000..357ed70 --- /dev/null +++ b/spotisub/templates/_flash_message.html @@ -0,0 +1,11 @@ + +{% with messages = get_flashed_messages() %} + {% if messages %} +
+ {% for message in messages %} + {{ message }} + {% endfor %} +
+ {% endif %} +{% endwith %} + \ No newline at end of file diff --git a/spotisub/templates/base.html b/spotisub/templates/base.html new file mode 100644 index 0000000..691d111 --- /dev/null +++ b/spotisub/templates/base.html @@ -0,0 +1,176 @@ + + + + + + + + + + {% block title %} + {% if title %} + {{ title }} - Fixed Sidebar + {% else %} + Fixed Sidebar Dashboard + {% endif %} + {% endblock %} + + + + + + + + + + + + + + + {% block navbar %} + {% if current_user.is_authenticated %} +
+ + + + +
+ + + + {% block current_user_content %} + + {% endblock %} +
+
+ {% else %} + + {% endif %} + {% endblock %} + + {% block content %} +
+ + {% block app_content %}{% endblock %} +
+ {% endblock %} + + {% block scripts %} + + + + + + + + + + + {% endblock %} + + + \ No newline at end of file diff --git a/spotisub/templates/dashboard.html b/spotisub/templates/dashboard.html index 22e18aa..6c4f39d 100644 --- a/spotisub/templates/dashboard.html +++ b/spotisub/templates/dashboard.html @@ -1 +1,95 @@ -work in progress. \ No newline at end of file +{% extends 'base.html' %} + +{% block current_user_content %} + + {% include '_flash_message.html' %} + + +

Collapsible Sidebar Using Bootstrap 4

+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis + nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore + eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt + in culpa qui officia deserunt mollit anim id est laborum. +

+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis + nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore + eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt + in culpa qui officia deserunt mollit anim id est laborum. +

+ +
+ +

Lorem Ipsum Dolor

+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis + nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore + eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt + in culpa qui officia deserunt mollit anim id est laborum. +

+ +
+ +

Lorem Ipsum Dolor

+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis + nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore + eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt + in culpa qui officia deserunt mollit anim id est laborum. +

+ +
+ +

Lorem Ipsum Dolor

+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis + nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore + eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt + in culpa qui officia deserunt mollit anim id est laborum. +

+
+ +

Lorem Ipsum Dolor

+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis + nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore + eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt + in culpa qui officia deserunt mollit anim id est laborum. +

+ +
+ +

Lorem Ipsum Dolor

+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis + nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore + eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt + in culpa qui officia deserunt mollit anim id est laborum. +

+
+ +

Lorem Ipsum Dolor

+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis + nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore + eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt + in culpa qui officia deserunt mollit anim id est laborum. +

+{% endblock %} \ No newline at end of file diff --git a/spotisub/templates/errors/404.html b/spotisub/templates/errors/404.html new file mode 100644 index 0000000..0f6009a --- /dev/null +++ b/spotisub/templates/errors/404.html @@ -0,0 +1,21 @@ +{% extends 'base.html' %} + +{% if current_user.is_authenticated %} + {% block current_user_content %} +

{{ title }}

+

+ Return to the home page +

+ {% endblock %} +{% else %} + {% block app_context %} +
+
+

{{ title }}

+

+ Return to the home page +

+
+
+ {% endblock %} +{% endif %} \ No newline at end of file diff --git a/spotisub/templates/errors/500.html b/spotisub/templates/errors/500.html new file mode 100644 index 0000000..0f6009a --- /dev/null +++ b/spotisub/templates/errors/500.html @@ -0,0 +1,21 @@ +{% extends 'base.html' %} + +{% if current_user.is_authenticated %} + {% block current_user_content %} +

{{ title }}

+

+ Return to the home page +

+ {% endblock %} +{% else %} + {% block app_context %} +
+
+

{{ title }}

+

+ Return to the home page +

+
+
+ {% endblock %} +{% endif %} \ No newline at end of file diff --git a/spotisub/templates/login.html b/spotisub/templates/login.html new file mode 100644 index 0000000..324377e --- /dev/null +++ b/spotisub/templates/login.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% block app_content %} +
+
+ + {% include '_flash_message.html' %} + +

{{ title }}

+
+
+
+
+ +
+
+ {{ wtf.quick_form(form) }} +

+ New here? Register. +

+
+
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/spotisub/templates/register.html b/spotisub/templates/register.html new file mode 100644 index 0000000..7ef219c --- /dev/null +++ b/spotisub/templates/register.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% block app_content %} +
+
+ + {% include '_flash_message.html' %} + +

{{ title }}

+
+
+
+
+ +
+
+ {{ wtf.quick_form(form) }} +
+
+ +
+
+{% endblock %} \ No newline at end of file From 41e70e51cd5a4e746f9348878a1b4c10d12e5f71 Mon Sep 17 00:00:00 2001 From: blast Date: Sun, 22 Sep 2024 14:45:48 +0200 Subject: [PATCH 07/36] implementing first working dashboard template, moved APIs to /api/v1 --- Dockerfile | 2 +- config.py | 5 +++- requirements.txt | 22 +++++++++++++--- spotisub/__init__.py | 2 +- spotisub/database.py | 15 +++++++++-- spotisub/errors.py | 13 ++++++++++ spotisub/forms.py | 20 ++++++++++++++ spotisub/routes.py | 62 +++++++++++++++++++++++++++++++++++++++----- 8 files changed, 127 insertions(+), 14 deletions(-) create mode 100644 spotisub/errors.py create mode 100644 spotisub/forms.py diff --git a/Dockerfile b/Dockerfile index 70adff5..b99ca82 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ ENV PATH="/home/uwsgi/.local/bin:${PATH}" COPY requirements.txt ./ RUN pip3 install --no-cache-dir -r requirements.txt -COPY main.py init.py entrypoint.sh first_run.sh uwsgi.ini ./ +COPY main.py config.py init.py entrypoint.sh first_run.sh uwsgi.ini ./ COPY spotisub spotisub/ USER root diff --git a/config.py b/config.py index 07830e8..cdc91ac 100644 --- a/config.py +++ b/config.py @@ -6,7 +6,10 @@ class Config(object): """All application configurations""" + # Secret key + SECRET_KEY = os.environ.get('SECRET_KEY') + # Database configurations SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ - 'sqlite:///' + os.path.join(basedir, 'cache/configration.db') + 'sqlite:///' + os.path.join(basedir, 'cache/spotisub.sqlite3') SQLALCHEMY_TRACK_MODIFICATIONS = False diff --git a/requirements.txt b/requirements.txt index 65a7476..b5ac3b3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,16 +3,32 @@ python-dotenv==1.0.0 werkzeug==2.2.2 Flask~=2.2.2 flask-restx==1.3.0 -click<8.1.0 +click==8.1.3 uWSGI==2.0.21 Flask-Login==0.6.2 Flask-SQLAlchemy==3.0.2 Flask-Bootstrap==3.3.7.1 Flask-APScheduler==1.12.4 +Flask-WTF==1.0.1 py-sonic ffmpeg spotdl sqlalchemy==2.0.7 pyarr -expiringdict -musicbrainzngs \ No newline at end of file +expiringdict==1.2.2 +musicbrainzngs +alembic==1.8.1 +dnspython==2.2.1 +dominate==2.7.0 +email-validator==1.3.0 +greenlet==2.0.0.post0 +idna==3.4 +importlib-metadata==5.0.0 +importlib-resources==5.10.0 +itsdangerous==2.1.2 +Jinja2==3.1.2 +Mako==1.2.3 +MarkupSafe==2.1.1 +visitor==0.1.3 +WTForms==3.0.1 +zipp==3.10.0 \ No newline at end of file diff --git a/spotisub/__init__.py b/spotisub/__init__.py index 6d9046b..69ee9b2 100644 --- a/spotisub/__init__.py +++ b/spotisub/__init__.py @@ -33,4 +33,4 @@ login = LoginManager(spotisub) login.login_view = 'login' -from spotisub import routes \ No newline at end of file +from spotisub import routes, classes, errors \ No newline at end of file diff --git a/spotisub/database.py b/spotisub/database.py index 6b34490..e5f6e58 100644 --- a/spotisub/database.py +++ b/spotisub/database.py @@ -6,12 +6,14 @@ from sqlalchemy import delete from sqlalchemy import Table from sqlalchemy import Column +from sqlalchemy import Integer from sqlalchemy import String from sqlalchemy import MetaData from sqlalchemy import DateTime from sqlalchemy.sql import func SQLITE = 'sqlite' +USER = 'user' SUBSONIC_PLAYLIST = 'subsonic_playlist' SUBSONIC_SONG = 'subsonic_song' SUBSONIC_ARTIST = 'subsonic_artist' @@ -39,6 +41,15 @@ def __init__(self, dbtype, dbname=''): metadata = MetaData() + user = Table(USER, metadata, + Column( + 'id', Integer, primary_key=True, nullable=False), + Column( + 'username', String(36), unique=True, index=True, nullable=False), + Column( + 'password_hash', String(128), nullable=False) + ) + subsonic_spotify_relation = Table(SUBSONIC_SPOTIFY_RELATION, metadata, Column( 'uuid', String(36), primary_key=True, nullable=False), @@ -200,8 +211,8 @@ def select_all_playlists(missing_only): dbms.subsonic_spotify_relation.c.subsonic_artist_id, dbms.subsonic_spotify_relation.c.subsonic_playlist_id, dbms.subsonic_spotify_relation.c.spotify_song_uuid).where( - dbms.subsonic_spotify_relation.c.subsonic_song_id == None, - dbms.subsonic_spotify_relation.c.subsonic_artist_id == None) + dbms.subsonic_spotify_relation.c.subsonic_song_id is None, + dbms.subsonic_spotify_relation.c.subsonic_artist_id is None) else: stmt = select( dbms.subsonic_spotify_relation.c.uuid, diff --git a/spotisub/errors.py b/spotisub/errors.py new file mode 100644 index 0000000..66080ef --- /dev/null +++ b/spotisub/errors.py @@ -0,0 +1,13 @@ +from flask import render_template +from spotisub import spotisub, configuration_db + + +@spotisub.errorhandler(404) +def not_found_error(error): + return render_template('errors/404.html', title='Page Not Found'), 404 + + +@spotisub.errorhandler(500) +def internal_error(error): + configuration_db.session.rollback() + return render_template('errors/500.html', title='Unexpected Error'), 500 diff --git a/spotisub/forms.py b/spotisub/forms.py new file mode 100644 index 0000000..5ad15ed --- /dev/null +++ b/spotisub/forms.py @@ -0,0 +1,20 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, PasswordField, BooleanField +from wtforms.validators import DataRequired, Length, EqualTo + + +class LoginForm(FlaskForm): + """Login Form""" + username = StringField('Username', validators=[DataRequired(), Length(1, 64)]) + password = PasswordField('Password', validators=[DataRequired()]) + remember_me = BooleanField('Keep me logged in') + submit = SubmitField('Log In') + + +class RegistrationForm(FlaskForm): + """Registration Form""" + username = StringField('Username', validators=[DataRequired(), Length(1, 64)]) + password = PasswordField('Password', validators=[DataRequired()]) + confirm_password = PasswordField( + 'Confirm Password', validators=[DataRequired(), EqualTo('password')]) + submit = SubmitField('Register') diff --git a/spotisub/routes.py b/spotisub/routes.py index 6af2cf4..8347c3b 100644 --- a/spotisub/routes.py +++ b/spotisub/routes.py @@ -5,13 +5,24 @@ import threading import json from time import strftime +from flask import Blueprint from flask import Response from flask import request from flask import render_template +from flask import url_for +from flask import redirect +from flask import flash from flask_restx import Api from flask_restx import Resource +from flask_login import current_user +from flask_login import login_user +from flask_login import logout_user +from flask_login import login_required from flask_apscheduler import APScheduler -from spotisub import spotisub +from spotisub import spotisub, configuration_db +from spotisub.forms import LoginForm +from spotisub.forms import RegistrationForm +from spotisub.classes import User from spotisub import utils from spotisub import constants from spotisub import generator @@ -35,9 +46,9 @@ def after_request(response): response.status) return response - -api = Api(spotisub) - +blueprint = Blueprint('api', __name__, url_prefix='/api/v1') +api = Api(blueprint, doc='/docs/') +spotisub.register_blueprint(blueprint) def get_response_json(data, status): """Generates json response""" @@ -56,10 +67,49 @@ def get_json_message(message, is_ok): return json.dumps(data) +@spotisub.route('/') @spotisub.route('/dashboard') +@login_required def dashboard(): - """Dashboard path""" - return render_template('dashboard.html', data=[]) + return render_template('dashboard.html', title='Dashboard') + + +@spotisub.route('/login', methods=['GET', 'POST']) +def login(): + if current_user.is_authenticated: + return redirect(url_for('profile')) + form = LoginForm() + if form.validate_on_submit(): + user = User.query.filter_by(username=form.username.data).first() + if user is None or not user.check_password(form.password.data): + flash('Invalid username or password') + return redirect(url_for('login')) + login_user(user, remember=form.remember_me.data) + flash(f'Welcome {user.username}') + return redirect(url_for('dashboard')) + return render_template('login.html', title='Login', form=form) + + +@spotisub.route('/register',methods=['GET', 'POST']) +def register(): + if current_user.is_authenticated: + return redirect(url_for('profile')) + form = RegistrationForm() + if form.validate_on_submit(): + user = User(username=form.username.data) + user.set_password(form.password.data) + configuration_db.session.add(user) + configuration_db.session.commit() + flash('Registered successfully. Please log in to continue') + return redirect(url_for('login')) + return render_template('register.html', title='Register', form=form) + +@spotisub.route('/logout') +@login_required +def logout(): + """Used to log out a user""" + logout_user() + return redirect(url_for('login')) nsgenerate = api.namespace('generate', 'Generate APIs') From e480181aeae60e75421bfe25f7ef9b97a9eae497 Mon Sep 17 00:00:00 2001 From: blast Date: Sun, 22 Sep 2024 15:28:04 +0200 Subject: [PATCH 08/36] Renaming components --- spotisub/static/css/current_user_main.css | 20 +++++- spotisub/templates/base.html | 86 +++++++++++++---------- spotisub/templates/dashboard.html | 84 ++-------------------- 3 files changed, 72 insertions(+), 118 deletions(-) diff --git a/spotisub/static/css/current_user_main.css b/spotisub/static/css/current_user_main.css index 2f21b1b..2fc9cc5 100644 --- a/spotisub/static/css/current_user_main.css +++ b/spotisub/static/css/current_user_main.css @@ -258,4 +258,22 @@ a.article:hover { #sidebarCollapse span { display: none; } -} \ No newline at end of file +} + +#search { + display: block; + grid-area: search; + grid-template: + "search" 60px + / 420px; + justify-content: center; + align-content: center; + justify-items: stretch; + align-items: stretch; + background: hsl(0, 0%, 99%); + padding: 0 30px 0 60px; + border: 1px; + border-radius: 100px; + font: 24px/1 system-ui, sans-serif; + outline-offset: -8px; +} diff --git a/spotisub/templates/base.html b/spotisub/templates/base.html index 691d111..78ed3ce 100644 --- a/spotisub/templates/base.html +++ b/spotisub/templates/base.html @@ -9,9 +9,9 @@ {% block title %} {% if title %} - {{ title }} - Fixed Sidebar + {{ title }} - Spotisub {% else %} - Fixed Sidebar Dashboard + Spotisub Dashboard {% endif %} {% endblock %} @@ -32,68 +32,76 @@ {% if current_user.is_authenticated %}
-