add method and route to search across tracks, albums and artists.

+ break models into separate files
+ same for the utils and setup
This commit is contained in:
geoffrey45
2023-03-09 13:08:50 +03:00
parent d39c0ea2f8
commit e3ec9db989
55 changed files with 1113 additions and 1137 deletions
+48 -12
View File
@@ -9,9 +9,9 @@ from tqdm import tqdm
from requests.exceptions import ConnectionError as ReqConnError, ReadTimeout
from app import settings
from app.models import Artist
from app.db.store import Store
from app.utils import create_hash
from app.models import Artist, Track, Album
from app.db import store
from app.utils.hashing import create_hash
def get_artist_image_link(artist: str):
@@ -38,6 +38,7 @@ def get_artist_image_link(artist: str):
return None
# TODO: Move network calls to utils/network.py
class DownloadImage:
def __init__(self, url: str, name: str) -> None:
sm_path = Path(settings.ARTIST_IMG_SM_PATH) / name
@@ -71,8 +72,8 @@ class CheckArtistImages:
with ThreadPoolExecutor() as pool:
list(
tqdm(
pool.map(self.download_image, Store.artists),
total=len(Store.artists),
pool.map(self.download_image, store.Store.artists),
total=len(store.Store.artists),
desc="Downloading artist images",
)
)
@@ -95,13 +96,9 @@ class CheckArtistImages:
return DownloadImage(url, name=f"{artist.artisthash}.webp")
# def fetch_album_bio(title: str, albumartist: str) -> str | None:
# """
# Returns the album bio for a given album.
# """
# last_fm_url = "http://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key={}&artist={}&album={}&format=json".format(
# settings.LAST_FM_API_KEY, albumartist, title
# )
# def fetch_album_bio(title: str, albumartist: str) -> str | None: """ Returns the album bio for a given album. """
# last_fm_url = "http://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key={}&artist={}&album={
# }&format=json".format( settings.LAST_FM_API_KEY, albumartist, title )
# try:
# response = requests.get(last_fm_url)
@@ -128,3 +125,42 @@ class CheckArtistImages:
# def __call__(self):
# return fetch_album_bio(self.title, self.albumartist)
def get_artists_from_tracks(tracks: list[Track]) -> set[str]:
"""
Extracts all artists from a list of tracks. Returns a list of Artists.
"""
artists = set()
master_artist_list = [[x.name for x in t.artist] for t in tracks]
artists = artists.union(*master_artist_list)
return artists
def get_albumartists(albums: list[Album]) -> set[str]:
artists = set()
for album in albums:
albumartists = [a.name for a in album.albumartists]
artists.update(albumartists)
return artists
def get_all_artists(
tracks: list[Track], albums: list[Album]
) -> list[Artist]:
artists_from_tracks = get_artists_from_tracks(tracks=tracks)
artist_from_albums = get_albumartists(albums=albums)
artists = list(artists_from_tracks.union(artist_from_albums))
artists = sorted(artists)
lower_artists = set(a.lower().strip() for a in artists)
indices = [[ar.lower().strip() for ar in artists].index(a) for a in lower_artists]
artists = [artists[i] for i in indices]
return [Artist(a) for a in artists]
+1 -1
View File
@@ -5,7 +5,7 @@ from app.db.store import Store
from app.models import Folder, Track
from app.settings import SUPPORTED_FILES
from app.logger import log
from app.utils import win_replace_slash
from app.utils.wintools import win_replace_slash
class GetFilesAndDirs:
+27 -32
View File
@@ -4,13 +4,11 @@ This library contains all the functions related to playlists.
import os
import random
import string
from datetime import datetime
from typing import Any
from PIL import Image, ImageSequence
from app import settings
from app.logger import log
def create_thumbnail(image: Any, img_path: str) -> str:
@@ -80,36 +78,33 @@ def save_p_image(file, pid: str):
return filename
class ValidatePlaylistThumbs:
"""
Removes all unused images in the images/playlists folder.
"""
def __init__(self) -> None:
images = []
playlists = Get.get_all_playlists()
log.info("Validating playlist thumbnails")
for playlist in playlists:
if playlist.image:
img_path = playlist.image.split("/")[-1]
thumb_path = playlist.thumb.split("/")[-1]
images.append(img_path)
images.append(thumb_path)
p_path = os.path.join(settings.APP_DIR, "images", "playlists")
for image in os.listdir(p_path):
if image not in images:
os.remove(os.path.join(p_path, image))
log.info("Validating playlist thumbnails ... ✅")
def create_new_date():
return datetime.now()
#
# class ValidatePlaylistThumbs:
# """
# Removes all unused images in the images/playlists folder.
# """
#
# def __init__(self) -> None:
# images = []
# playlists = Get.get_all_playlists()
#
# log.info("Validating playlist thumbnails")
# for playlist in playlists:
# if playlist.image:
# img_path = playlist.image.split("/")[-1]
# thumb_path = playlist.thumb.split("/")[-1]
#
# images.append(img_path)
# images.append(thumb_path)
#
# p_path = os.path.join(settings.APP_DIR, "images", "playlists")
#
# for image in os.listdir(p_path):
# if image not in images:
# os.remove(os.path.join(p_path, image))
#
# log.info("Validating playlist thumbnails ... ✅")
#
# TODO: Fix ValidatePlaylistThumbs
+1 -2
View File
@@ -11,7 +11,7 @@ from app.lib.colorlib import ProcessAlbumColors, ProcessArtistColors
from app.lib.taglib import extract_thumb, get_tags
from app.logger import log
from app.models import Album, Artist, Track
from app.utils import run_fast_scandir
from app.utils.filesystem import run_fast_scandir
get_all_tracks = SQLiteTrackMethods.get_all_tracks
insert_many_tracks = SQLiteTrackMethods.insert_many_tracks
@@ -72,7 +72,6 @@ class Populate:
ProcessAlbumColors()
ProcessArtistColors()
@staticmethod
def filter_untagged(tracks: list[Track], files: list[str]):
tagged_files = [t.filepath for t in tracks]
+101 -11
View File
@@ -1,12 +1,15 @@
"""
This library contains all the functions related to the search functionality.
"""
from typing import List
from typing import List, Generator, TypeVar, Any
import itertools
from rapidfuzz import fuzz, process
from unidecode import unidecode
from app import models
from app.db.store import Store
from app.utils.remove_duplicates import remove_duplicates
ratio = fuzz.ratio
wratio = fuzz.WRatio
@@ -35,31 +38,32 @@ class Limit:
class SearchTracks:
def __init__(self, tracks: List[models.Track], query: str) -> None:
def __init__(self, query: str) -> None:
self.query = query
self.tracks = tracks
self.tracks = Store.tracks
def __call__(self) -> List[models.Track]:
"""
Gets all songs with a given title.
"""
tracks = [unidecode(track.og_title).lower() for track in self.tracks]
track_titles = [unidecode(track.og_title).lower() for track in self.tracks]
results = process.extract(
self.query,
tracks,
track_titles,
scorer=fuzz.WRatio,
score_cutoff=Cutoff.tracks,
limit=Limit.tracks,
)
return [self.tracks[i[2]] for i in results]
tracks = [self.tracks[i[2]] for i in results]
return remove_duplicates(tracks)
class SearchArtists:
def __init__(self, artists: list[models.Artist], query: str) -> None:
def __init__(self, query: str) -> None:
self.query = query
self.artists = artists
self.artists = Store.artists
def __call__(self) -> list:
"""
@@ -75,14 +79,13 @@ class SearchArtists:
limit=Limit.artists,
)
artists = [a[0] for a in results]
return [self.artists[i[2]] for i in results]
class SearchAlbums:
def __init__(self, albums: List[models.Album], query: str) -> None:
def __init__(self, query: str) -> None:
self.query = query
self.albums = albums
self.albums = Store.albums
def __call__(self) -> List[models.Album]:
"""
@@ -125,3 +128,90 @@ class SearchPlaylists:
)
return [self.playlists[i[2]] for i in results]
_type = List[models.Track | models.Album | models.Artist]
_S2 = TypeVar("_S2")
_ResultType = int | float
def get_titles(items: _type):
for item in items:
if isinstance(item, models.Track):
text = item.og_title
elif isinstance(item, models.Album):
text = item.title
# print(text)
elif isinstance(item, models.Artist):
text = item.name
else:
text = None
yield text
class SearchAll:
"""
Joins all tracks, albums and artists
then fuzzy searches them as a single unit.
"""
@staticmethod
def collect_all():
all_items: _type = []
all_items.extend(Store.tracks)
all_items.extend(Store.albums)
all_items.extend(Store.artists)
return all_items, get_titles(all_items)
@staticmethod
def get_results(items: Generator[str, Any, None], query: str):
items = list(items)
results = process.extract(
query=query,
choices=items,
scorer=fuzz.WRatio,
score_cutoff=Cutoff.tracks,
limit=20
)
return results
@staticmethod
def sort_results(items: _type):
"""
Separates results into differrent lists using itertools.groupby.
"""
mapped_items = [
{"type": "track", "item": item} if isinstance(item, models.Track) else
{"type": "album", "item": item} if isinstance(item, models.Album) else
{"type": "artist", "item": item} if isinstance(item, models.Artist) else
{"type": "Unknown", "item": item} for item in items
]
mapped_items.sort(key=lambda x: x["type"])
groups = [
list(group) for key, group in
itertools.groupby(mapped_items, lambda x: x["type"])
]
print(len(groups))
# merge items of a group into a dict that looks like: {"albums": [album1, ...]}
groups = [
{f"{group[0]['type']}s": [i['item'] for i in group]} for group in groups
]
return groups
@staticmethod
def search(query: str):
items, titles = SearchAll.collect_all()
results = SearchAll.get_results(titles, query)
results = [items[i[2]] for i in results]
return SearchAll.sort_results(results)
+3 -6
View File
@@ -6,12 +6,9 @@ from PIL import Image, UnidentifiedImageError
from tinytag import TinyTag
from app import settings
from app.utils import (
create_hash,
parse_artist_from_filename,
parse_title_from_filename,
win_replace_slash,
)
from app.utils.hashing import create_hash
from app.utils.parsers import parse_title_from_filename, parse_artist_from_filename
from app.utils.wintools import win_replace_slash
def parse_album_art(filepath: str):
+1
View File
@@ -15,5 +15,6 @@ def validate_tracks() -> None:
"""
for track in tqdm(Store.tracks, desc="Removing deleted tracks"):
if not os.path.exists(track.filepath):
print(f"Removing {track.filepath}")
Store.tracks.remove(track)
tdb.remove_track_by_filepath(track.filepath)
+3 -1
View File
@@ -8,7 +8,6 @@ import time
from watchdog.events import PatternMatchingEventHandler
from watchdog.observers import Observer
from app.logger import log
from app.db.store import Store
from app.lib.taglib import get_tags
@@ -91,6 +90,9 @@ class Watcher:
"WatchdogError: Failed to start watchdog, root directories could not be resolved."
)
return
except OSError as e:
log.error('Failed to start watchdog. %s', e)
return
try:
while True: