Skip to content

Commit

Permalink
pgtrm search command
Browse files Browse the repository at this point in the history
  • Loading branch information
AdrienPensart committed Nov 7, 2023
1 parent 345f3d9 commit ada6d72
Show file tree
Hide file tree
Showing 14 changed files with 3,800 additions and 51 deletions.
3 changes: 3 additions & 0 deletions dbschema/default.esdl
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ module default {
type Album {
# required user: User;
required name: str;
property title := (select .artist.name ++ " - " ++ .name);
required created_at: datetime {
default := std::datetime_current();
readonly := true;
Expand Down Expand Up @@ -178,6 +179,7 @@ module default {
type Music {
# required user: User;
required name: str;
property title := (select .album.title ++ " - " ++ .name);
required created_at: datetime {
default := std::datetime_current();
readonly := true;
Expand Down Expand Up @@ -309,4 +311,5 @@ module default {

using extension graphql;
using extension edgeql_http;
using extension pg_trgm;
# using extension auth;
5 changes: 5 additions & 0 deletions dbschema/migrations/00003.edgeql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE MIGRATION m1mawhozxatfw7ecqk4ln2jv7rakw4x4ff342vyjorz7y2u7itbs4q
ONTO m1vvmk4g5onoi2ja7ote3xorjxb7x5w7phvywwcaxx36py6zqgjssq
{
CREATE EXTENSION pg_trgm VERSION '1.6';
};
9 changes: 9 additions & 0 deletions dbschema/migrations/00004.edgeql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE MIGRATION m1vkbcskhpl6xb77uzkdp6axbq7uudh72kwvwldvwj7wx5aw32yzna
ONTO m1mawhozxatfw7ecqk4ln2jv7rakw4x4ff342vyjorz7y2u7itbs4q
{
ALTER TYPE default::Music {
CREATE PROPERTY title := (SELECT
((((.artist.name ++ ' - ') ++ .album.name) ++ ' - ') ++ .name)
);
};
};
16 changes: 16 additions & 0 deletions dbschema/migrations/00005.edgeql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
CREATE MIGRATION m1ll44jx2l454tqbeaqe6c4kssjsd3gaiuwaom5jtki22dvbgisbfa
ONTO m1vkbcskhpl6xb77uzkdp6axbq7uudh72kwvwldvwj7wx5aw32yzna
{
ALTER TYPE default::Album {
CREATE PROPERTY title := (SELECT
((.artist.name ++ ' - ') ++ .name)
);
};
ALTER TYPE default::Music {
ALTER PROPERTY title {
USING (SELECT
((.album.title ++ ' - ') ++ .name)
);
};
};
};
4 changes: 4 additions & 0 deletions musicbot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
from musicbot.musicdb import MusicDb
from musicbot.object import MusicbotObject
from musicbot.playlist import Playlist
from musicbot.playlist_options import PlaylistOptions
from musicbot.scan_folders import ScanFolders
from musicbot.search_results import SearchResults

filterwarnings(action="ignore", module=".*vlc.*", category=DeprecationWarning)

Expand All @@ -19,7 +21,9 @@
"Music",
"MusicFilter",
"Playlist",
"PlaylistOptions",
"MusicDb",
"Folder",
"ScanFolders",
"SearchResults",
]
6 changes: 3 additions & 3 deletions musicbot/commands/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def ui(ctx: click.Context, musicdb: MusicDb, edgedb_args: tuple[str, ...]) -> No
@syncify
@beartype
async def clean(musicdb: MusicDb) -> None:
await musicdb.clean_musics()
_ = await musicdb.clean_musics()


@cli.command(help="Drop entire schema from DB")
Expand All @@ -106,12 +106,12 @@ async def clean(musicdb: MusicDb) -> None:
@syncify
@beartype
async def drop(musicdb: MusicDb) -> None:
await musicdb.drop()
_ = await musicdb.drop()


@cli.command(help="Clean entities without musics associated")
@musicdb_options
@syncify
@beartype
async def soft_clean(musicdb: MusicDb) -> None:
await musicdb.soft_clean()
_ = await musicdb.soft_clean()
43 changes: 31 additions & 12 deletions musicbot/commands/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@
from rich.table import Table
from watchfiles import Change, DefaultFilter, awatch

from musicbot import (
File,
MusicbotObject,
MusicDb,
MusicFilter,
Playlist,
PlaylistOptions,
ScanFolders,
)
from musicbot.cli.file import flat_option
from musicbot.cli.music_filter import filters_reprs, music_filters_options
from musicbot.cli.musicdb import musicdb_options
Expand All @@ -32,13 +41,7 @@
scan_folders_argument,
)
from musicbot.defaults import DEFAULT_VLC_PARAMS
from musicbot.file import File
from musicbot.helpers import syncify
from musicbot.music_filter import MusicFilter
from musicbot.musicdb import MusicDb
from musicbot.object import MusicbotObject
from musicbot.playlist_options import PlaylistOptions
from musicbot.scan_folders import ScanFolders

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -99,7 +102,7 @@ async def remove(
musicdb: MusicDb,
) -> None:
for file in files:
await musicdb.remove_music_path(file)
_ = await musicdb.remove_music_path(file)


@cli.command(help="Clean all musics", aliases=["wipe"])
Expand All @@ -109,7 +112,7 @@ async def remove(
async def clean(
musicdb: MusicDb,
) -> None:
await musicdb.clean_musics()
_ = await musicdb.clean_musics()


@cli.command(help="Load musics")
Expand Down Expand Up @@ -143,13 +146,13 @@ async def scan(
# check=False,
# )
if clean:
await musicdb.clean_musics()
_ = await musicdb.clean_musics()

files = await musicdb.upsert_folders(
scan_folders=scan_folders,
coroutines=coroutines,
)
await musicdb.soft_clean()
_ = await musicdb.soft_clean()

if output == "json":
MusicbotObject.print_json([asdict(file.music) for file in files if file.music is not None])
Expand Down Expand Up @@ -207,7 +210,7 @@ def __call__(self, change: Change, path: str) -> bool:
if change in (Change.added, Change.modified):
await update_music(path)
elif change == Change.deleted:
await musicdb.remove_music_path(path)
_ = await musicdb.remove_music_path(path)
except edgedb.ClientConnectionFailedTemporarilyError as error:
MusicbotObject.err(f"{musicdb} : unable to clean musics", error=error)
except (asyncio.CancelledError, KeyboardInterrupt):
Expand Down Expand Up @@ -236,12 +239,28 @@ async def search(
pattern: str,
playlist_options: PlaylistOptions,
) -> None:
p = await musicdb.search(pattern)
search_results = await musicdb.search(pattern)
p = Playlist.from_edgedb(
name=pattern,
results=search_results.musics,
)
p.print(
output=output,
playlist_options=playlist_options,
)

artists = [artist.name for artist in search_results.artists]
MusicbotObject.success(f"Artists found: {artists}")

albums = [album.name for album in search_results.albums]
MusicbotObject.success(f"Albums found: {albums}")

genres = [genre.name for genre in search_results.genres]
MusicbotObject.success(f"Genres found: {genres}")

keywords = [keyword.name for keyword in search_results.keywords]
MusicbotObject.success(f"Keywords found: {keywords}")


@cli.command(short_help="Generate a new playlist", help=filters_reprs)
@musicdb_options
Expand Down
14 changes: 7 additions & 7 deletions musicbot/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,13 @@ def canonic_artist_album_filename(self) -> PurePath:
def filename(self) -> str:
return self.path.name

@property
def canonic_title(self) -> str:
prefix = f"{str(self.track).zfill(2)} - "
if not self.title.startswith(prefix):
return f"{prefix}{self.title}"
return self.title

@property
def canonic_filename(self) -> str:
return f"{self.canonic_title}{self.extension}"
Expand Down Expand Up @@ -229,13 +236,6 @@ def title(self, title: str) -> None:
else:
self.handle.tags.add(id3.TIT2(text=title))

@property
def canonic_title(self) -> str:
prefix = f"{str(self.track).zfill(2)} - "
if not self.title.startswith(prefix):
return f"{prefix}{self.title}"
return self.title

@property
def album(self) -> str:
if self.extension == ".flac":
Expand Down
34 changes: 18 additions & 16 deletions musicbot/musicdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import edgedb
import httpx
from async_lru import alru_cache
from attr import asdict
from attr import asdict as attr_asdict
from beartype import beartype
from edgedb.asyncio_client import AsyncIOClient, create_async_client
from edgedb.options import RetryOptions, TransactionOptions
Expand All @@ -32,6 +32,7 @@
UPSERT_QUERY,
)
from musicbot.scan_folders import ScanFolders
from musicbot.search_results import SearchResults

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -120,15 +121,15 @@ async def execute_music_filters(
self,
query: str,
music_filters: frozenset[MusicFilter] = frozenset(),
) -> set[Any]:
) -> set[edgedb.Object]:
logger.debug(query)
results = set()
if not music_filters:
music_filters = frozenset([MusicFilter()])
for music_filter in music_filters:
intermediate_results = await self.client.query(
query,
**asdict(music_filter),
**attr_asdict(music_filter),
)
logger.debug(f"{len(intermediate_results)} intermediate results for {music_filter}")
results.update(intermediate_results)
Expand All @@ -142,22 +143,22 @@ async def make_playlist(
results = await self.execute_music_filters(PLAYLIST_QUERY, music_filters)
return Playlist.from_edgedb(
name=" | ".join([music_filter.help_repr() for music_filter in music_filters]),
results=results,
results=list(results),
)

async def clean_musics(self) -> Any:
async def clean_musics(self) -> None | list[edgedb.Object]:
query = """delete Artist;"""
if self.dry:
return None
return await self.client.query(query)

async def drop(self) -> Any:
async def drop(self) -> None | list[edgedb.Object]:
query = """reset schema to initial;"""
if self.dry:
return None
return await self.client.query(query)

async def soft_clean(self) -> Any:
async def soft_clean(self) -> None | edgedb.Object:
self.success("cleaning orphan musics, artists, albums, genres, keywords")
if self.dry:
return None
Expand All @@ -166,7 +167,7 @@ async def soft_clean(self) -> Any:
async def ensure_connected(self) -> AsyncIOClient:
return await self.client.ensure_connected()

async def remove_music_path(self, path: str) -> Any:
async def remove_music_path(self, path: str) -> None | edgedb.Object:
logger.debug(f"{self} : removed {path}")
if self.dry:
return None
Expand Down Expand Up @@ -307,12 +308,13 @@ async def make_bests(
async def search(
self,
pattern: str,
) -> Playlist:
# results = await self.client.query_single(query=SEARCH_QUERY, pattern=pattern)
results = await self.client.query(query=SEARCH_QUERY, pattern=pattern)
playlist = Playlist.from_edgedb(
name=pattern,
# results=results.musics,
results=results,
) -> SearchResults:
result = await self.client.query_single(query=SEARCH_QUERY, pattern=pattern)
search_results = SearchResults(
musics=result.musics,
artists=result.artists,
albums=result.albums,
genres=result.genres,
keywords=result.keywords,
)
return playlist
return search_results
6 changes: 4 additions & 2 deletions musicbot/playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from dataclasses import asdict, dataclass
from typing import Any, Self

import edgedb
from beartype import beartype
from click_skeleton.helpers import PrettyDefaultDict
from click_skeleton.helpers import seconds_to_human as formatted_seconds_to_human
Expand Down Expand Up @@ -44,12 +45,13 @@ def from_files(
def from_edgedb(
cls,
name: str,
results: Any,
results: list[edgedb.Object],
) -> Self:
musics = []
for result in results:
keywords = frozenset(keyword.name for keyword in result.keywords)
folders = frozenset(Folder(path=folder["@path"], name=folder.name, ipv4=folder.ipv4, username=folder.username) for folder in result.folders)
# folders = frozenset(Folder(path=folder["@path"], name=folder.name, ipv4=folder.ipv4, username=folder.username) for folder in result.folders)
folders = frozenset(Folder(path=folder.path, name=folder.name, ipv4=folder.ipv4, username=folder.username) for folder in result.folders)
music = Music(
title=result.name,
artist=result.artist.name,
Expand Down
33 changes: 23 additions & 10 deletions musicbot/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,18 +74,31 @@ class CustomStringTemplate(string.Template):
# keywords := (select distinct keywords_res.object {name})
# }
# """

# SEARCH_QUERY: str = CustomStringTemplate(
# """
# select Music {
# #music_fields
# }
# filter
# .name ilike <str>$pattern or
# .genre.name ilike <str>$pattern or
# .album.name ilike <str>$pattern or
# .artist.name ilike <str>$pattern or
# .keywords.name ilike <str>$pattern or
# .paths ilike "%" ++ <str>$pattern ++ "%"
# """
# ).substitute(music_fields=MUSIC_FIELDS)

SEARCH_QUERY: str = CustomStringTemplate(
"""
select Music {
#music_fields
}
filter
.name ilike <str>$pattern or
.genre.name ilike <str>$pattern or
.album.name ilike <str>$pattern or
.artist.name ilike <str>$pattern or
.keywords.name ilike <str>$pattern or
.paths ilike "%" ++ <str>$pattern ++ "%"
select {
musics := (select Music{#music_fields} filter ext::pg_trgm::word_similar(<str>$pattern, Music.title)),
albums := (select Album{*} filter ext::pg_trgm::word_similar(<str>$pattern, Album.title)),
artists := (select Artist{*} filter ext::pg_trgm::word_similar(<str>$pattern, Artist.name)),
genres := (select Genre{*} filter ext::pg_trgm::word_similar(<str>$pattern, Genre.name)),
keywords := (select Keyword{*} filter ext::pg_trgm::word_similar(<str>$pattern, Keyword.name)),
};
"""
).substitute(music_fields=MUSIC_FIELDS)

Expand Down
Loading

0 comments on commit ada6d72

Please sign in to comment.