support in_quotes search query

This commit is contained in:
mungai-njoroge 2023-08-06 22:09:39 +03:00
parent 943d6e3590
commit f28d3f00bd
6 changed files with 238 additions and 174 deletions

View File

@ -20,16 +20,6 @@ api = Blueprint("playlist", __name__, url_prefix="/")
PL = SQLitePlaylistMethods
insert_one_playlist = PL.insert_one_playlist
get_playlist_by_name = PL.get_playlist_by_name
count_playlist_by_name = PL.count_playlist_by_name
get_all_playlists = PL.get_all_playlists
get_playlist_by_id = PL.get_playlist_by_id
tracks_to_playlist = PL.add_tracks_to_playlist
update_playlist = PL.update_playlist
delete_playlist = PL.delete_playlist
remove_image = PL.remove_banner
def duplicate_images(images: list):
if len(images) == 1:
@ -80,7 +70,7 @@ def send_all_playlists():
# get the no_images query param
no_images = request.args.get("no_images", False)
playlists = get_all_playlists()
playlists = PL.get_all_playlists()
playlists = list(playlists)
for playlist in playlists:
@ -109,7 +99,7 @@ def insert_playlist(name: str):
),
}
return insert_one_playlist(playlist)
return PL.insert_one_playlist(playlist)
@api.route("/playlist/new", methods=["POST"])
@ -122,7 +112,7 @@ def create_playlist():
if data is None:
return {"error": "Playlist name not provided"}, 400
existing_playlist_count = count_playlist_by_name(data["name"])
existing_playlist_count = PL.count_playlist_by_name(data["name"])
if existing_playlist_count > 0:
return {"error": "Playlist already exists"}, 409
@ -161,7 +151,7 @@ def get_artist_trackhashes(artisthash: str):
@api.route("/playlist/<playlist_id>/add", methods=["POST"])
def add_track_to_playlist(playlist_id: str):
def add_item_to_playlist(playlist_id: str):
"""
Takes a playlist ID and a track hash, and adds the track to the playlist
"""
@ -191,10 +181,10 @@ def add_track_to_playlist(playlist_id: str):
else:
trackhashes = []
insert_count = tracks_to_playlist(int(playlist_id), trackhashes)
insert_count = PL.add_tracks_to_playlist(int(playlist_id), trackhashes)
if insert_count == 0:
return {"error": "Track already exists in playlist"}, 409
return {"error": "Item already exists in playlist"}, 409
PL.update_last_updated(int(playlist_id))
@ -209,7 +199,7 @@ def get_playlist(playlistid: str):
no_tracks = request.args.get("no_tracks", False)
no_tracks = no_tracks == "true"
playlist = get_playlist_by_id(int(playlistid))
playlist = PL.get_playlist_by_id(int(playlistid))
if playlist is None:
return {"msg": "Playlist not found"}, 404
@ -243,7 +233,7 @@ def update_playlist_info(playlistid: str):
if playlistid is None:
return {"error": "Playlist ID not provided"}, 400
db_playlist = get_playlist_by_id(int(playlistid))
db_playlist = PL.get_playlist_by_id(int(playlistid))
if db_playlist is None:
return {"error": "Playlist not found"}, 404
@ -279,7 +269,7 @@ def update_playlist_info(playlistid: str):
p_tuple = (*playlist.values(),)
update_playlist(int(playlistid), playlist)
PL.update_playlist(int(playlistid), playlist)
playlist = models.Playlist(*p_tuple)
playlist.last_updated = date_string_to_time_passed(playlist.last_updated)
@ -295,12 +285,12 @@ def remove_playlist_image(playlistid: str):
Removes the playlist image.
"""
pid = int(playlistid)
playlist = get_playlist_by_id(pid)
playlist = PL.get_playlist_by_id(pid)
if playlist is None:
return {"error": "Playlist not found"}, 404
remove_image(pid)
PL.remove_image(pid)
playlist.image = None
playlist.thumb = None
@ -330,7 +320,7 @@ def remove_playlist():
except KeyError:
return message, 400
delete_playlist(pid)
PL.delete_playlist(pid)
return {"msg": "Done"}, 200
@ -373,7 +363,7 @@ def remove_tracks_from_playlist(pid: int):
def playlist_exists(name: str) -> bool:
return count_playlist_by_name(name) > 0
return PL.count_playlist_by_name(name) > 0
@api.route("/playlist/save-item", methods=["POST"])
@ -402,7 +392,12 @@ def save_item_as_playlist():
except KeyError:
itemhash = None
if itemtype == "folder":
if itemtype is None or playlist_name is None or itemhash is None:
return msg
if itemtype == "track":
trackhashes = [itemhash]
elif itemtype == "folder":
trackhashes = get_path_trackhashes(itemhash)
elif itemtype == "album":
trackhashes = get_album_trackhashes(itemhash)
@ -419,7 +414,7 @@ def save_item_as_playlist():
if playlist is None:
return {"error": "Playlist could not be created"}, 500
tracks_to_playlist(playlist.id, trackhashes)
PL.add_tracks_to_playlist(playlist.id, trackhashes)
PL.update_last_updated(playlist.id)
return {"playlist_id": playlist.id}, 201
return {"playlist": playlist}, 201

View File

@ -2,13 +2,11 @@
Contains all the search routes.
"""
from unidecode import unidecode
from flask import Blueprint, request
from unidecode import unidecode
from app import models
from app.lib import searchlib
from app.store.tracks import TrackStore
api = Blueprint("search", __name__, url_prefix="/")
@ -17,68 +15,45 @@ SEARCH_COUNT = 12
"""The max amount of items to return per request"""
class SearchResults:
def query_in_quotes(query: str) -> bool:
"""
Holds all the search results.
Returns True if the query is in quotes
"""
query: str = ""
tracks: list[models.Track] = []
albums: list[models.Album] = []
playlists: list[models.Playlist] = []
artists: list[models.Artist] = []
return query.startswith('"') and query.endswith('"')
class Search:
def __init__(self, query: str) -> None:
self.tracks: list[models.Track] = []
self.query = unidecode(query)
SearchResults.query = self.query
def search_tracks(self):
def search_tracks(self, in_quotes=False):
"""
Calls :class:`SearchTracks` which returns the tracks that fuzzily match
the search terms. Then adds them to the `SearchResults` store.
"""
self.tracks = TrackStore.tracks
tracks = searchlib.SearchTracks(self.query)()
SearchResults.tracks = tracks
return tracks
return searchlib.TopResults().search(
self.query, tracks_only=True, in_quotes=in_quotes
)
def search_artists(self):
"""Calls :class:`SearchArtists` which returns the artists that fuzzily match
the search term. Then adds them to the `SearchResults` store.
"""
artists = searchlib.SearchArtists(self.query)()
SearchResults.artists = artists
return artists
return searchlib.SearchArtists(self.query)()
def search_albums(self):
def search_albums(self, in_quotes=False):
"""Calls :class:`SearchAlbums` which returns the albums that fuzzily match
the search term. Then adds them to the `SearchResults` store.
"""
albums = searchlib.SearchAlbums(self.query)()
SearchResults.albums = albums
return albums
# def search_playlists(self):
# """Calls :class:`SearchPlaylists` which returns the playlists that fuzzily match
# the search term. Then adds them to the `SearchResults` store.
# """
# playlists = utils.Get.get_all_playlists()
# playlists = [serializer.Playlist(playlist) for playlist in playlists]
# playlists = searchlib.SearchPlaylists(playlists, self.query)()
# SearchResults.playlists = playlists
# return playlists
def get_top_results(self):
finder = searchlib.SearchAll()
return finder.search(self.query)
return searchlib.TopResults().search(
self.query, albums_only=True, in_quotes=in_quotes
)
def get_top_results(self, in_quotes=False):
finder = searchlib.TopResults()
return finder.search(self.query, in_quotes=in_quotes)
@api.route("/search/tracks", methods=["GET"])
@ -88,10 +63,12 @@ def search_tracks():
"""
query = request.args.get("q")
in_quotes = query_in_quotes(query)
if not query:
return {"error": "No query provided"}, 400
tracks = Search(query).search_tracks()
tracks = Search(query).search_tracks(in_quotes)
return {
"tracks": tracks[:SEARCH_COUNT],
@ -106,14 +83,16 @@ def search_albums():
"""
query = request.args.get("q")
in_quotes = query_in_quotes(query)
if not query:
return {"error": "No query provided"}, 400
tracks = Search(query).search_albums()
albums = Search(query).search_albums(in_quotes)
return {
"albums": tracks[:SEARCH_COUNT],
"more": len(tracks) > SEARCH_COUNT,
"albums": albums[:SEARCH_COUNT],
"more": len(albums) > SEARCH_COUNT,
}
@ -124,6 +103,7 @@ def search_artists():
"""
query = request.args.get("q")
if not query:
return {"error": "No query provided"}, 400
@ -135,24 +115,6 @@ def search_artists():
}
# @searchbp.route("/search/playlists", methods=["GET"])
# def search_playlists():
# """
# Searches for playlists.
# """
# query = request.args.get("q")
# if not query:
# return {"error": "No query provided"}, 400
# playlists = DoSearch(query).search_playlists()
# return {
# "playlists": playlists[:SEARCH_COUNT],
# "more": len(playlists) > SEARCH_COUNT,
# }
@api.route("/search/top", methods=["GET"])
def get_top_results():
"""
@ -160,21 +122,12 @@ def get_top_results():
"""
query = request.args.get("q")
in_quotes = query_in_quotes(query)
if not query:
return {"error": "No query provided"}, 400
results = Search(query).get_top_results()
# max_results = 2
# return {
# "tracks": SearchResults.tracks[:max_results],
# "albums": SearchResults.albums[:max_results],
# "artists": SearchResults.artists[:max_results],
# "playlists": SearchResults.playlists[:max_results],
# }
return {
"results": results
}
return Search(query).get_top_results(in_quotes=in_quotes)
@api.route("/search/loadmore")
@ -182,28 +135,32 @@ def search_load_more():
"""
Returns more songs, albums or artists from a search query.
"""
query = request.args.get("q")
in_quotes = query_in_quotes(query)
s_type = request.args.get("type")
index = int(request.args.get("index") or 0)
if s_type == "tracks":
t = SearchResults.tracks
t = Search(query).search_tracks(in_quotes)
return {
"tracks": t[index: index + SEARCH_COUNT],
"tracks": t[index : index + SEARCH_COUNT],
"more": len(t) > index + SEARCH_COUNT,
}
elif s_type == "albums":
a = SearchResults.albums
a = Search(query).search_albums(in_quotes)
return {
"albums": a[index: index + SEARCH_COUNT],
"albums": a[index : index + SEARCH_COUNT],
"more": len(a) > index + SEARCH_COUNT,
}
elif s_type == "artists":
a = SearchResults.artists
a = Search(query).search_artists()
return {
"artists": a[index: index + SEARCH_COUNT],
"artists": a[index : index + SEARCH_COUNT],
"more": len(a) > index + SEARCH_COUNT,
}
# TODO: Rewrite this file using generators where possible
# TODO: Rewrite this file using generators where possible

View File

@ -77,7 +77,9 @@ class CheckArtistImages:
with Pool(cpu_count()) as pool:
res = list(
tqdm(
pool.imap_unordered(self.download_image, artist_store.ArtistStore.artists),
pool.imap_unordered(
self.download_image, artist_store.ArtistStore.artists
),
total=len(artist_store.ArtistStore.artists),
desc="Downloading missing artist images",
)
@ -164,10 +166,17 @@ def get_all_artists(tracks: list[Track], albums: list[Album]) -> list[Artist]:
artist_from_albums = get_albumartists(albums=albums)
artists = list(artists_from_tracks.union(artist_from_albums))
artists = sorted(artists)
artists.sort()
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]
# Remove duplicates
artists_dup_free = set()
artist_hashes = set()
return [Artist(a) for a in artists]
for artist in artists:
artist_hash = create_hash(artist, decode=True)
if artist_hash not in artist_hashes:
artists_dup_free.add(artist)
artist_hashes.add(artist_hash)
return [Artist(a) for a in artists_dup_free]

View File

@ -1,21 +1,23 @@
"""
This library contains all the functions related to the search functionality.
"""
from typing import List, Generator, TypeVar, Any
import itertools
from typing import Any, Generator, List, TypeVar
from rapidfuzz import fuzz, process
from rapidfuzz import process, utils
from unidecode import unidecode
from app import models
from app.utils.remove_duplicates import remove_duplicates
from app.models.enums import FavType
from app.models.track import Track
from app.store.albums import AlbumStore
from app.store.artists import ArtistStore
from app.store.tracks import TrackStore
ratio = fuzz.ratio
wratio = fuzz.WRatio
from app.utils.remove_duplicates import remove_duplicates
from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb
# ratio = fuzz.ratio
# wratio = fuzz.WRatio
class Cutoff:
@ -56,6 +58,7 @@ class SearchTracks:
track_titles,
score_cutoff=Cutoff.tracks,
limit=Limit.tracks,
processor=utils.default_process,
)
tracks = [self.tracks[i[2]] for i in results]
@ -67,7 +70,7 @@ class SearchArtists:
self.query = query
self.artists = ArtistStore.artists
def __call__(self) -> list:
def __call__(self):
"""
Gets all artists with a given name.
"""
@ -78,6 +81,7 @@ class SearchArtists:
artists,
score_cutoff=Cutoff.artists,
limit=Limit.artists,
processor=utils.default_process,
)
return [self.artists[i[2]] for i in results]
@ -100,17 +104,11 @@ class SearchAlbums:
albums,
score_cutoff=Cutoff.albums,
limit=Limit.albums,
processor=utils.default_process,
)
return [self.albums[i[2]] for i in results]
# get all artists that matched the query
# for get all albums from the artists
# get all albums that matched the query
# return [**artist_albums **albums]
# recheck next and previous artist on play next or add to playlist
class SearchPlaylists:
def __init__(self, playlists: List[models.Playlist], query: str) -> None:
@ -124,12 +122,13 @@ class SearchPlaylists:
playlists,
score_cutoff=Cutoff.playlists,
limit=Limit.playlists,
processor=utils.default_process,
)
return [self.playlists[i[2]] for i in results]
_type = List[models.Track | models.Album | models.Artist]
_type = models.Track | models.Album | models.Artist
_S2 = TypeVar("_S2")
_ResultType = int | float
@ -148,7 +147,7 @@ def get_titles(items: _type):
yield text
class SearchAll:
class TopResults:
"""
Joins all tracks, albums and artists
then fuzzy searches them as a single unit.
@ -156,11 +155,11 @@ class SearchAll:
@staticmethod
def collect_all():
all_items: _type = []
all_items: list[_type] = []
all_items.extend(ArtistStore.artists)
all_items.extend(TrackStore.tracks)
all_items.extend(AlbumStore.albums)
all_items.extend(ArtistStore.artists)
return all_items, get_titles(all_items)
@ -169,44 +168,157 @@ class SearchAll:
items = list(items)
results = process.extract(
query=query,
choices=items,
score_cutoff=Cutoff.tracks,
limit=20
query=query, choices=items, score_cutoff=Cutoff.tracks, limit=1
)
return results
@staticmethod
def sort_results(items: _type):
def map_with_type(item: _type):
"""
Separates results into differrent lists using itertools.groupby.
Map the results to their respective types.
"""
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
]
if isinstance(item, models.Track):
return {"type": "track", "item": item}
mapped_items.sort(key=lambda x: x["type"])
if isinstance(item, models.Album):
tracks = TrackStore.get_tracks_by_albumhash(item.albumhash)
tracks = remove_duplicates(tracks)
groups = [
list(group) for key, group in
itertools.groupby(mapped_items, lambda x: x["type"])
]
item.get_date_from_tracks(tracks)
try:
item.duration = sum((t.duration for t in tracks))
except AttributeError:
item.duration = 0
# 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
]
item.check_is_single(tracks)
return groups
if not item.is_single:
item.check_type()
item.is_favorite = favdb.check_is_favorite(
item.albumhash, fav_type=FavType.album
)
return {"type": "album", "item": item}
if isinstance(item, models.Artist):
track_count = 0
duration = 0
for track in TrackStore.get_tracks_by_artisthash(item.artisthash):
track_count += 1
duration += track.duration
album_count = AlbumStore.count_albums_by_artisthash(item.artisthash)
item.set_trackcount(track_count)
item.set_albumcount(album_count)
item.set_duration(duration)
return {"type": "artist", "item": item}
@staticmethod
def search(query: str):
items, titles = SearchAll.collect_all()
results = SearchAll.get_results(titles, query)
results = [items[i[2]] for i in results]
def get_track_items(item: dict[str, _type], query: str, limit=5):
tracks: list[Track] = []
return SearchAll.sort_results(results)
if item["type"] == "track":
tracks.extend(SearchTracks(query)())
if item["type"] == "album":
t = TrackStore.get_tracks_by_albumhash(item["item"].albumhash)
t.sort(key=lambda x: x.last_mod)
# if there are less than the limit, get more tracks
if len(t) < limit:
remainder = limit - len(t)
more_tracks = SearchTracks(query)()
t.extend(more_tracks[:remainder])
tracks.extend(t)
if item["type"] == "artist":
t = TrackStore.get_tracks_by_artisthash(item["item"].artisthash)
t.sort(key=lambda x: x.last_mod)
# if there are less than the limit, get more tracks
if len(t) < limit:
remainder = limit - len(t)
more_tracks = SearchTracks(query)()
t.extend(more_tracks[:remainder])
tracks.extend(t)
return tracks[:limit]
@staticmethod
def get_album_items(item: dict[str, _type], query: str, limit=6):
if item["type"] == "track":
return SearchAlbums(query)()[:limit]
if item["type"] == "album":
return SearchAlbums(query)()[:limit]
if item["type"] == "artist":
albums = AlbumStore.get_albums_by_artisthash(item["item"].artisthash)
# if there are less than the limit, get more albums
if len(albums) < limit:
remainder = limit - len(albums)
more_albums = SearchAlbums(query)()
albums.extend(more_albums[:remainder])
return albums[:limit]
@staticmethod
def search(query: str, albums_only=False, tracks_only=False, in_quotes=False):
items, titles = TopResults.collect_all()
results = TopResults.get_results(titles, query)
tracks_limit = Limit.tracks if tracks_only else 5
albums_limit = Limit.albums if albums_only else 6
artists_limit = 3
# map results to their respective items
try:
result = [items[i[2]] for i in results][0]
except IndexError:
if tracks_only:
return []
if albums_only:
return []
return {
"top_result": None,
"tracks": [],
"artists": [],
"albums": [],
}
result = TopResults.map_with_type(result)
if in_quotes:
top_tracks = SearchTracks(query)()[:tracks_limit]
else:
top_tracks = TopResults.get_track_items(result, query, limit=tracks_limit)
if tracks_only:
return top_tracks
if in_quotes:
albums = SearchAlbums(query)()[:albums_limit]
else:
albums = TopResults.get_album_items(result, query, limit=albums_limit)
if albums_only:
return albums
artists = SearchArtists(query)()[:artists_limit]
return {
"top_result": result,
"tracks": top_tracks,
"artists": artists,
"albums": albums,
}

View File

@ -10,12 +10,10 @@ from watchdog.events import PatternMatchingEventHandler
from watchdog.observers import Observer
from app import settings
from app.db.sqlite.albumcolors import SQLiteAlbumMethods as aldb
from app.db.sqlite.settings import SettingsSQLMethods as sdb
from app.db.sqlite.tracks import SQLiteManager
from app.db.sqlite.tracks import SQLiteTrackMethods as db
from app.db.sqlite.albumcolors import SQLiteAlbumMethods as aldb
from app.lib.colorlib import process_color
from app.lib.taglib import extract_thumb, get_tags
from app.logger import log
@ -199,7 +197,7 @@ def remove_track(filepath: str) -> None:
db.remove_tracks_by_filepaths(filepath)
TrackStore.remove_track_by_filepath(filepath)
empty_album = TrackStore.count_tracks_by_hash(track.albumhash) > 0
empty_album = TrackStore.count_tracks_by_trackhash(track.albumhash) > 0
if empty_album:
AlbumStore.remove_album_by_hash(track.albumhash)

View File

@ -84,18 +84,11 @@ class TrackStore:
tdb.remove_tracks_by_folders(to_remove)
@classmethod
def count_tracks_by_hash(cls, trackhash: str) -> int:
def count_tracks_by_trackhash(cls, trackhash: str) -> int:
"""
Counts the number of tracks with a specific hash.
Counts the number of tracks with a specific trackhash.
"""
count = 0
for track in cls.tracks:
if track.trackhash == trackhash:
count += 1
return count
return sum(1 for track in cls.tracks if track.trackhash == trackhash)
@classmethod
def make_track_fav(cls, trackhash: str):