mirror of
https://github.com/tcsenpai/swingmusic.git
synced 2025-06-06 19:25:34 +00:00
add route to trigger Populate
+ use instance keys to stop multiple instances of populate + move Populate error to a new file + misc
This commit is contained in:
parent
8b25a9265f
commit
4271a6f4a0
@ -223,3 +223,18 @@ def set_setting():
|
|||||||
value = ",".join(value)
|
value = ",".join(value)
|
||||||
|
|
||||||
return {"result": value}
|
return {"result": value}
|
||||||
|
|
||||||
|
|
||||||
|
@background
|
||||||
|
def run_populate():
|
||||||
|
populate.Populate(instance_key=get_random_str())
|
||||||
|
|
||||||
|
|
||||||
|
@api.route("/settings/trigger-scan", methods=["GET"])
|
||||||
|
def trigger_scan():
|
||||||
|
"""
|
||||||
|
Triggers a scan.
|
||||||
|
"""
|
||||||
|
run_populate()
|
||||||
|
|
||||||
|
return {"msg": "Scan triggered!"}
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import urllib
|
import urllib
|
||||||
from concurrent.futures import ProcessPoolExecutor as Pool
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from multiprocessing import Pool, cpu_count
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from PIL import Image, UnidentifiedImageError
|
from PIL import Image, UnidentifiedImageError
|
||||||
from requests.exceptions import ConnectionError as ReqConnError
|
from requests.exceptions import ConnectionError as RequestConnectionError
|
||||||
from requests.exceptions import ReadTimeout
|
from requests.exceptions import ReadTimeout
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
@ -16,6 +15,9 @@ from app.store import artists as artist_store
|
|||||||
from app.utils.hashing import create_hash
|
from app.utils.hashing import create_hash
|
||||||
|
|
||||||
|
|
||||||
|
CHECK_ARTIST_IMAGES_KEY = ""
|
||||||
|
|
||||||
|
|
||||||
def get_artist_image_link(artist: str):
|
def get_artist_image_link(artist: str):
|
||||||
"""
|
"""
|
||||||
Returns an artist image url.
|
Returns an artist image url.
|
||||||
@ -36,7 +38,7 @@ def get_artist_image_link(artist: str):
|
|||||||
return res["picture_big"]
|
return res["picture_big"]
|
||||||
|
|
||||||
return None
|
return None
|
||||||
except (ReqConnError, ReadTimeout, IndexError, KeyError):
|
except (RequestConnectionError, ReadTimeout, IndexError, KeyError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -73,13 +75,18 @@ class DownloadImage:
|
|||||||
|
|
||||||
|
|
||||||
class CheckArtistImages:
|
class CheckArtistImages:
|
||||||
def __init__(self):
|
def __init__(self, instance_key: str):
|
||||||
with Pool(cpu_count()) as pool:
|
global CHECK_ARTIST_IMAGES_KEY
|
||||||
|
CHECK_ARTIST_IMAGES_KEY = instance_key
|
||||||
|
|
||||||
|
key_artist_map = (
|
||||||
|
(instance_key, artist) for artist in artist_store.ArtistStore.artists
|
||||||
|
)
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=4) as executor:
|
||||||
res = list(
|
res = list(
|
||||||
tqdm(
|
tqdm(
|
||||||
pool.imap_unordered(
|
executor.map(self.download_image, key_artist_map),
|
||||||
self.download_image, artist_store.ArtistStore.artists
|
|
||||||
),
|
|
||||||
total=len(artist_store.ArtistStore.artists),
|
total=len(artist_store.ArtistStore.artists),
|
||||||
desc="Downloading missing artist images",
|
desc="Downloading missing artist images",
|
||||||
)
|
)
|
||||||
@ -88,12 +95,17 @@ class CheckArtistImages:
|
|||||||
list(res)
|
list(res)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def download_image(artist: Artist):
|
def download_image(_map: tuple[str, Artist]):
|
||||||
"""
|
"""
|
||||||
Checks if an artist image exists and downloads it if not.
|
Checks if an artist image exists and downloads it if not.
|
||||||
|
|
||||||
:param artist: The artist name
|
:param artist: The artist name
|
||||||
"""
|
"""
|
||||||
|
instance_key, artist = _map
|
||||||
|
|
||||||
|
if CHECK_ARTIST_IMAGES_KEY != instance_key:
|
||||||
|
return
|
||||||
|
|
||||||
img_path = (
|
img_path = (
|
||||||
Path(settings.Paths.get_artist_img_sm_path()) / f"{artist.artisthash}.webp"
|
Path(settings.Paths.get_artist_img_sm_path()) / f"{artist.artisthash}.webp"
|
||||||
)
|
)
|
||||||
|
@ -15,6 +15,11 @@ from app.db.sqlite.utils import SQLiteManager
|
|||||||
|
|
||||||
from app.store.artists import ArtistStore
|
from app.store.artists import ArtistStore
|
||||||
from app.store.albums import AlbumStore
|
from app.store.albums import AlbumStore
|
||||||
|
from app.logger import log
|
||||||
|
from app.lib.errors import PopulateCancelledError
|
||||||
|
|
||||||
|
PROCESS_ALBUM_COLORS_KEY = ""
|
||||||
|
PROCESS_ARTIST_COLORS_KEY = ""
|
||||||
|
|
||||||
|
|
||||||
def get_image_colors(image: str, count=1) -> list[str]:
|
def get_image_colors(image: str, count=1) -> list[str]:
|
||||||
@ -52,7 +57,10 @@ class ProcessAlbumColors:
|
|||||||
Extracts the most dominant color from the album art and saves it to the database.
|
Extracts the most dominant color from the album art and saves it to the database.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self, instance_key: str) -> None:
|
||||||
|
global PROCESS_ALBUM_COLORS_KEY
|
||||||
|
PROCESS_ALBUM_COLORS_KEY = instance_key
|
||||||
|
|
||||||
albums = [
|
albums = [
|
||||||
a
|
a
|
||||||
for a in AlbumStore.albums
|
for a in AlbumStore.albums
|
||||||
@ -62,6 +70,15 @@ class ProcessAlbumColors:
|
|||||||
with SQLiteManager() as cur:
|
with SQLiteManager() as cur:
|
||||||
try:
|
try:
|
||||||
for album in tqdm(albums, desc="Processing missing album colors"):
|
for album in tqdm(albums, desc="Processing missing album colors"):
|
||||||
|
if PROCESS_ALBUM_COLORS_KEY != instance_key:
|
||||||
|
raise PopulateCancelledError(
|
||||||
|
"A newer 'ProcessAlbumColors' instance is running. Stopping this one."
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: Stop hitting the database for every album.
|
||||||
|
# Instead, fetch all the data from the database and
|
||||||
|
# check from memory.
|
||||||
|
|
||||||
exists = aldb.exists(album.albumhash, cur=cur)
|
exists = aldb.exists(album.albumhash, cur=cur)
|
||||||
if exists:
|
if exists:
|
||||||
continue
|
continue
|
||||||
@ -83,14 +100,22 @@ class ProcessArtistColors:
|
|||||||
Extracts the most dominant color from the artist art and saves it to the database.
|
Extracts the most dominant color from the artist art and saves it to the database.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self, instance_key: str) -> None:
|
||||||
all_artists = [a for a in ArtistStore.artists if len(a.colors) == 0]
|
all_artists = [a for a in ArtistStore.artists if len(a.colors) == 0]
|
||||||
|
|
||||||
|
global PROCESS_ARTIST_COLORS_KEY
|
||||||
|
PROCESS_ARTIST_COLORS_KEY = instance_key
|
||||||
|
|
||||||
with SQLiteManager() as cur:
|
with SQLiteManager() as cur:
|
||||||
try:
|
try:
|
||||||
for artist in tqdm(
|
for artist in tqdm(
|
||||||
all_artists, desc="Processing missing artist colors"
|
all_artists, desc="Processing missing artist colors"
|
||||||
):
|
):
|
||||||
|
if PROCESS_ARTIST_COLORS_KEY != instance_key:
|
||||||
|
raise PopulateCancelledError(
|
||||||
|
"A newer 'ProcessArtistColors' instance is running. Stopping this one."
|
||||||
|
)
|
||||||
|
|
||||||
exists = adb.exists(artist.artisthash, cur=cur)
|
exists = adb.exists(artist.artisthash, cur=cur)
|
||||||
|
|
||||||
if exists:
|
if exists:
|
||||||
|
7
app/lib/errors.py
Normal file
7
app/lib/errors.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
class PopulateCancelledError(Exception):
|
||||||
|
"""
|
||||||
|
Raised when the instance key of a looping function called
|
||||||
|
inside Populate is changed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
|
||||||
from requests import ConnectionError as RequestConnectionError
|
from requests import ConnectionError as RequestConnectionError
|
||||||
@ -14,6 +15,7 @@ from app.db.sqlite.tracks import SQLiteTrackMethods
|
|||||||
from app.lib.albumslib import validate_albums
|
from app.lib.albumslib import validate_albums
|
||||||
from app.lib.artistlib import CheckArtistImages
|
from app.lib.artistlib import CheckArtistImages
|
||||||
from app.lib.colorlib import ProcessAlbumColors, ProcessArtistColors
|
from app.lib.colorlib import ProcessAlbumColors, ProcessArtistColors
|
||||||
|
from app.lib.errors import PopulateCancelledError
|
||||||
from app.lib.taglib import extract_thumb, get_tags
|
from app.lib.taglib import extract_thumb, get_tags
|
||||||
from app.lib.trackslib import validate_tracks
|
from app.lib.trackslib import validate_tracks
|
||||||
from app.logger import log
|
from app.logger import log
|
||||||
@ -33,10 +35,6 @@ remove_tracks_by_filepaths = SQLiteTrackMethods.remove_tracks_by_filepaths
|
|||||||
POPULATE_KEY = ""
|
POPULATE_KEY = ""
|
||||||
|
|
||||||
|
|
||||||
class PopulateCancelledError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Populate:
|
class Populate:
|
||||||
"""
|
"""
|
||||||
Populates the database with all songs in the music directory
|
Populates the database with all songs in the music directory
|
||||||
@ -84,31 +82,39 @@ class Populate:
|
|||||||
|
|
||||||
self.extract_thumb_with_overwrite(modified_tracks)
|
self.extract_thumb_with_overwrite(modified_tracks)
|
||||||
|
|
||||||
ProcessTrackThumbnails()
|
try:
|
||||||
ProcessAlbumColors()
|
ProcessTrackThumbnails(instance_key)
|
||||||
ProcessArtistColors()
|
ProcessAlbumColors(instance_key)
|
||||||
|
ProcessArtistColors(instance_key)
|
||||||
|
except PopulateCancelledError as e:
|
||||||
|
log.warn(e)
|
||||||
|
return
|
||||||
|
|
||||||
tried_to_download_new_images = False
|
tried_to_download_new_images = False
|
||||||
|
|
||||||
if Ping()():
|
if Ping()():
|
||||||
tried_to_download_new_images = True
|
tried_to_download_new_images = True
|
||||||
try:
|
try:
|
||||||
CheckArtistImages()
|
CheckArtistImages(instance_key)
|
||||||
except (RequestConnectionError, ReadTimeout) as e:
|
except (RequestConnectionError, ReadTimeout) as e:
|
||||||
log.error(
|
log.error(
|
||||||
"Internet connection lost. Downloading artist images stopped."
|
"Internet connection lost. Downloading artist images stopped."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
log.warning(
|
log.warning(
|
||||||
f"No internet connection. Downloading artist images halted for {settings.get_scan_sleep_time()} seconds."
|
f"No internet connection. Downloading artist images stopped!"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Re-process the new artist images.
|
# Re-process the new artist images.
|
||||||
if tried_to_download_new_images:
|
if tried_to_download_new_images:
|
||||||
ProcessArtistColors()
|
ProcessArtistColors(instance_key=instance_key)
|
||||||
|
|
||||||
if Ping()():
|
if Ping()():
|
||||||
FetchSimilarArtistsLastFM()
|
try:
|
||||||
|
FetchSimilarArtistsLastFM(instance_key)
|
||||||
|
except PopulateCancelledError as e:
|
||||||
|
log.warn(e)
|
||||||
|
return
|
||||||
|
|
||||||
ArtistStore.load_artists(instance_key)
|
ArtistStore.load_artists(instance_key)
|
||||||
|
|
||||||
@ -151,7 +157,8 @@ class Populate:
|
|||||||
|
|
||||||
for file in tqdm(untagged, desc="Reading files"):
|
for file in tqdm(untagged, desc="Reading files"):
|
||||||
if POPULATE_KEY != key:
|
if POPULATE_KEY != key:
|
||||||
raise PopulateCancelledError("Populate key changed")
|
log.warning("'Populate.tag_untagged': Populate key changed")
|
||||||
|
return
|
||||||
|
|
||||||
tags = get_tags(file)
|
tags = get_tags(file)
|
||||||
|
|
||||||
@ -197,7 +204,7 @@ class Populate:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
||||||
def get_image(album: Album):
|
def get_image(_map: tuple[str, Album]):
|
||||||
"""
|
"""
|
||||||
The function retrieves an image from an album by iterating through its tracks and extracting the thumbnail from the first track that has one.
|
The function retrieves an image from an album by iterating through its tracks and extracting the thumbnail from the first track that has one.
|
||||||
|
|
||||||
@ -206,6 +213,11 @@ def get_image(album: Album):
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
instance_key, album = _map
|
||||||
|
|
||||||
|
if POPULATE_KEY != instance_key:
|
||||||
|
raise PopulateCancelledError("'ProcessTrackThumbnails': Populate key changed")
|
||||||
|
|
||||||
matching_tracks = filter(
|
matching_tracks = filter(
|
||||||
lambda t: t.albumhash == album.albumhash, TrackStore.tracks
|
lambda t: t.albumhash == album.albumhash, TrackStore.tracks
|
||||||
)
|
)
|
||||||
@ -226,7 +238,7 @@ def get_image(album: Album):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
from multiprocessing import Pool, cpu_count
|
CPU_COUNT = os.cpu_count() // 2
|
||||||
|
|
||||||
|
|
||||||
class ProcessTrackThumbnails:
|
class ProcessTrackThumbnails:
|
||||||
@ -234,11 +246,13 @@ class ProcessTrackThumbnails:
|
|||||||
Extracts the album art from all albums in album store.
|
Extracts the album art from all albums in album store.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self, instance_key: str) -> None:
|
||||||
with Pool(processes=cpu_count()) as pool:
|
key_album_map = ((instance_key, album) for album in AlbumStore.albums)
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=CPU_COUNT) as executor:
|
||||||
results = list(
|
results = list(
|
||||||
tqdm(
|
tqdm(
|
||||||
pool.imap_unordered(get_image, AlbumStore.albums),
|
executor.map(get_image, key_album_map),
|
||||||
total=len(AlbumStore.albums),
|
total=len(AlbumStore.albums),
|
||||||
desc="Extracting track images",
|
desc="Extracting track images",
|
||||||
)
|
)
|
||||||
@ -247,11 +261,18 @@ class ProcessTrackThumbnails:
|
|||||||
list(results)
|
list(results)
|
||||||
|
|
||||||
|
|
||||||
def save_similar_artists(artist: Artist):
|
def save_similar_artists(_map: tuple[str, Artist]):
|
||||||
"""
|
"""
|
||||||
Downloads and saves similar artists to the database.
|
Downloads and saves similar artists to the database.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
instance_key, artist = _map
|
||||||
|
|
||||||
|
if POPULATE_KEY != instance_key:
|
||||||
|
raise PopulateCancelledError(
|
||||||
|
"'FetchSimilarArtistsLastFM': Populate key changed"
|
||||||
|
)
|
||||||
|
|
||||||
if lastfmdb.exists(artist.artisthash):
|
if lastfmdb.exists(artist.artisthash):
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -266,17 +287,18 @@ def save_similar_artists(artist: Artist):
|
|||||||
|
|
||||||
class FetchSimilarArtistsLastFM:
|
class FetchSimilarArtistsLastFM:
|
||||||
"""
|
"""
|
||||||
Fetches similar artists from LastFM using a process pool.
|
Fetches similar artists from LastFM using a thread pool.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self, instance_key: str) -> None:
|
||||||
artists = ArtistStore.artists
|
artists = ArtistStore.artists
|
||||||
|
key_artist_map = ((instance_key, artist) for artist in artists)
|
||||||
|
|
||||||
with Pool(processes=cpu_count()) as pool:
|
with ThreadPoolExecutor(max_workers=CPU_COUNT) as executor:
|
||||||
try:
|
try:
|
||||||
results = list(
|
results = list(
|
||||||
tqdm(
|
tqdm(
|
||||||
pool.imap_unordered(save_similar_artists, artists),
|
executor.map(save_similar_artists, key_artist_map),
|
||||||
total=len(artists),
|
total=len(artists),
|
||||||
desc="Fetching similar artists",
|
desc="Fetching similar artists",
|
||||||
)
|
)
|
||||||
@ -284,6 +306,10 @@ class FetchSimilarArtistsLastFM:
|
|||||||
|
|
||||||
list(results)
|
list(results)
|
||||||
|
|
||||||
|
except PopulateCancelledError as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
# any exception that can be raised by the pool
|
# any exception that can be raised by the pool
|
||||||
except:
|
except Exception as e:
|
||||||
|
log.warn(e)
|
||||||
pass
|
pass
|
||||||
|
@ -7,7 +7,7 @@ from app.lib.populate import Populate, PopulateCancelledError
|
|||||||
from app.settings import SessionVarKeys, get_flag, get_scan_sleep_time
|
from app.settings import SessionVarKeys, get_flag, get_scan_sleep_time
|
||||||
from app.utils.generators import get_random_str
|
from app.utils.generators import get_random_str
|
||||||
from app.utils.threading import background
|
from app.utils.threading import background
|
||||||
|
from app.logger import log
|
||||||
|
|
||||||
@background
|
@background
|
||||||
def run_periodic_scans():
|
def run_periodic_scans():
|
||||||
@ -25,6 +25,7 @@ def run_periodic_scans():
|
|||||||
try:
|
try:
|
||||||
Populate(instance_key=get_random_str())
|
Populate(instance_key=get_random_str())
|
||||||
except PopulateCancelledError:
|
except PopulateCancelledError:
|
||||||
|
log.error("'run_periodic_scans': Periodic scan cancelled.")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
sleep_time = get_scan_sleep_time()
|
sleep_time = get_scan_sleep_time()
|
||||||
|
@ -168,7 +168,7 @@ class SessionVars:
|
|||||||
CLEAN_ALBUM_TITLE = True
|
CLEAN_ALBUM_TITLE = True
|
||||||
REMOVE_REMASTER_FROM_TRACK = True
|
REMOVE_REMASTER_FROM_TRACK = True
|
||||||
|
|
||||||
DO_PERIODIC_SCANS = True
|
DO_PERIODIC_SCANS = False
|
||||||
PERIODIC_SCAN_INTERVAL = 600 # 10 minutes
|
PERIODIC_SCAN_INTERVAL = 600 # 10 minutes
|
||||||
"""
|
"""
|
||||||
The interval between periodic scans in seconds.
|
The interval between periodic scans in seconds.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user