Skip to content

Commit

Permalink
new, ui: support albums
Browse files Browse the repository at this point in the history
  • Loading branch information
dxstiny committed Feb 8, 2024
1 parent 71bd141 commit 01af47a
Show file tree
Hide file tree
Showing 150 changed files with 660 additions and 269 deletions.
63 changes: 45 additions & 18 deletions src/server/db/table/albums.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,12 @@ def _fromDict(cls, data: JDict) -> Tuple[Any, ...] | None:
tracks.append(BasicSpotifyItem.fromDict(item))

releaseDateString = data.ensure("release_date", str, data.ensure("releaseDate", str))
releaseDate = datetime.fromisoformat(releaseDateString)
releaseDate = datetime.now()

try:
releaseDate = datetime.fromisoformat(releaseDateString)
except ValueError:
releaseDate = datetime.strptime(releaseDateString.split("-").pop(), "%Y")

return (
data.ensure("id", str),
Expand Down Expand Up @@ -341,7 +346,7 @@ def anyArtist(self) -> List[str]:
"""
for albums from multiple artists, with no common artist
"""
return self._anyArtist.split(",")
return self._anyArtist.split(",") if self._anyArtist else []

@anyArtist.setter
def anyArtist(self, value: List[str]) -> None:
Expand All @@ -352,7 +357,7 @@ def allArtists(self) -> List[str]:
"""
for albums from common artists
"""
return self._allArtists.split(",")
return self._allArtists.split(",") if self._allArtists else []

@allArtists.setter
def allArtists(self, value: List[str]) -> None:
Expand Down Expand Up @@ -393,13 +398,21 @@ def forceUpdate(self) -> None:
"""force update"""
self._fireChanged()

def toDict(self) -> Dict[str, Any]:
def toDict(self, songs: Optional[List[Song]] = None) -> Dict[str, Any]:
"""return dict"""
return {
value: Dict[str, Any] = {
"id": self.hash,
"href": f"/album/{self.hash}",
"name": self.name,
"spotify": self.spotify,
"image": self.image,
"artists": self.allArtists or self.anyArtist,
}
if not value["image"] and songs and len(songs) > 0:
value["image"] = songs[0].model.cover
if songs:
value["songs"] = [song.toDict() for song in songs]
return value

@staticmethod
async def _fetchMetadata(spotifyId: str, spotify: Spotify) -> Optional[SpotifyAlbum]:
Expand All @@ -423,11 +436,21 @@ async def _findAlbumBySong(song: Song) -> Optional[str]:
return None

@staticmethod
async def _findAlbumBySpotifySearch(albumName: str, spotify: Spotify) -> Optional[SpotifyAlbum]:
result = await asyncRunInThreadWithReturn(spotify.searchAlbum, albumName)
async def _findAlbumBySpotifySearch(song: Song, spotify: Spotify) -> Optional[SpotifyAlbum]:
result = await asyncRunInThreadWithReturn(
spotify.searchAlbum, f"{song.artist} {song.album}"
)
if result:
albums = result.unwrap()
firstAlbum = next((x for x in albums if x.name.lower() == albumName.lower()), None)
firstAlbum = next(
(
x
for x in albums
if x.name.lower() == song.album.lower()
and any(artist.name.lower() in song.artist.lower() for artist in x.artists)
),
None,
)
if firstAlbum:
return firstAlbum
return None
Expand All @@ -436,11 +459,16 @@ async def _findAlbumBySpotifySearch(albumName: str, spotify: Spotify) -> Optiona
async def _createModel(cls, song: Song, spotify: Spotify, db: Database) -> Optional[AlbumModel]:
logger = Logged.getLogger("createModel")
logger.debug("creating model for %s", song)
logger.debug("that would be %s", song.album)

model = await db.albums.byName(song.album)

logger.debug("model by name %s", model)
possibleModels = await db.albums.byName(song.album)
model = next(
(
x
for x in possibleModels
if any(artist in x.allArtists for artist in song.artist.split(","))
),
None,
)

alreadyExists = model is not None
albumId = model.spotifyModel.id if model and model.spotifyModel else None
Expand All @@ -452,7 +480,7 @@ async def _createModel(cls, song: Song, spotify: Spotify, db: Database) -> Optio
logger.debug("found album id: %s", albumId)
if not albumId:
logger.debug("album not found in db, searching spotify")
spotifyAlbum = await cls._findAlbumBySpotifySearch(song.album, spotify)
spotifyAlbum = await cls._findAlbumBySpotifySearch(song, spotify)
albumId = spotifyAlbum.id if spotifyAlbum else None
logger.debug("found album id: %s", albumId)
if not albumId:
Expand Down Expand Up @@ -484,8 +512,6 @@ async def _createModel(cls, song: Song, spotify: Spotify, db: Database) -> Optio
if newMetadata:
model.image = newMetadata.image

logger.debug("returning model %s", model)

if not alreadyExists:
model.id = await db.albums.insert(model)
return model
Expand All @@ -512,15 +538,16 @@ class AlbumsTable(ITable[AlbumModel]):
def _model(self) -> Type[AlbumModel]:
return AlbumModel

async def byName(self, name: str) -> Optional[AlbumModel]:
async def byName(self, name: str) -> List[AlbumModel]:
"""get artist by id"""
escaped = name.replace("'", "''")
where = f"name = '{escaped}'"
return await self.selectOne(append=f"WHERE {where}")
return await self.select(append=f"WHERE {where}")

async def byId(self, id_: str) -> Optional[AlbumModel]:
"""get artist by id"""
where = f"spotify LIKE '%{id_}%'"
(intId,) = hashids.decode(id_)
where = f"id = {intId}"
return await self.selectOne(append=f"WHERE {where}")

async def byArtist(self, artistName: str) -> List[AlbumModel]:
Expand Down
4 changes: 4 additions & 0 deletions src/server/db/table/songs.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,3 +371,7 @@ async def byArtist(self, artist: str) -> List[SongModel]:
return await self.select(
append=f"WHERE artist = '{artist}' or artist LIKE '%, {artist}, %' or artist LIKE '%, {artist}' or artist LIKE '{artist}, %' or spotify LIKE '%\"{artist}\"%' COLLATE NOCASE" # pylint: disable=line-too-long
)

async def byAlbum(self, albumHash: str) -> List[SongModel]:
"""get songs by album"""
return await self.select(append=f"WHERE albumHash = '{albumHash}'")
41 changes: 37 additions & 4 deletions src/server/handler/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from config.customData import LocalTrack, LocalCover


class MetaHandler:
class MetaHandler: # pylint: disable=too-many-public-methods
"""handler for different 'meta' features (e.g. metadata, spotify, search)"""

__slots__ = ("_spotify", "_dbManager", "_logger")
Expand Down Expand Up @@ -87,7 +87,7 @@ async def putArtist(
async def getArtists(self, _: web.Request) -> web.Response:
"""get(/api/artists)"""
artists = await self._dbManager.artists.all()
return web.json_response(data = [ artist.toDict() for artist in artists ])
return web.json_response(data=[artist.toDict() for artist in artists])

@withObjectPayload(Object({"name": String().min(1)}), inPath=True)
async def getArtist(
Expand All @@ -113,6 +113,39 @@ async def getArtist(
}
)

@withObjectPayload(Object({"albumHash": String().min(1)}), inPath=True)
async def putAlbum(
self, payload: Dict[str, Any]
) -> web.Response: # pylint: disable=too-many-locals
"""put(/api/albums/{albumHash})"""
albumHash: str = payload["albumHash"]
tracks = Song.list(await self._dbManager.songs.byAlbum(albumHash))
albumModel = await self._dbManager.albums.byId(albumHash)
if not albumModel:
return web.HTTPNotFound(text="album not found")
response = albumModel.toDict()
response["songs"] = [track.toDict() for track in tracks]
return web.json_response(data=response)

@withObjectPayload(Object({"albumHash": String().min(1)}), inPath=True)
async def getAlbum(
self, payload: Dict[str, Any]
) -> web.Response: # pylint: disable=too-many-locals
"""get(/api/albums/{albumHash})"""
albumHash: str = payload["albumHash"]
tracks = Song.list(await self._dbManager.songs.byAlbum(albumHash))
albumModel = await self._dbManager.albums.byId(albumHash)
if not albumModel:
return web.HTTPNotFound(text="album not found")
response = albumModel.toDict(tracks)
response["songs"] = [track.toDict() for track in tracks]
return web.json_response(data=response)

async def getAlbums(self, _: web.Request) -> web.Response:
"""get(/api/albums)"""
albums = await self._dbManager.albums.all()
return web.json_response(data=[album.toDict() for album in albums])

@withObjectPayload(
Object(
{
Expand Down Expand Up @@ -286,7 +319,7 @@ async def upload(self, request: web.Request) -> web.Response:
if not Runtime.args.withDocker:
return web.HTTPExpectationFailed(text="must run in docker")

async for obj in (await request.multipart()):
async for obj in await request.multipart():
if obj.filename:
file = LocalCover.createNew(obj.filename)
file.write(await obj.read())
Expand All @@ -298,7 +331,7 @@ async def uploadSong(self, request: web.Request) -> web.Response:
if not Runtime.args.withDocker:
return web.HTTPExpectationFailed(text="must run in docker")

async for obj in (await request.multipart()):
async for obj in await request.multipart():
if obj.filename:
file = LocalTrack.createNew(obj.filename)
file.write(await obj.read())
Expand Down
8 changes: 8 additions & 0 deletions src/server/handler/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ async def getPrevious(self, _: web.Request) -> web.Response:
Object({"type": String().enum("playlist"), "id": String()}),
Object({"type": String().enum("track"), "id": Integer()}),
Object({"type": String().enum("artist"), "name": String()}),
Object({"type": String().enum("album"), "id": String()}),
),
inBody=True,
)
Expand Down Expand Up @@ -70,6 +71,13 @@ async def loadPlaylist(self, payload: Dict[str, str]) -> web.Response:
asyncio.create_task(self._player.loadPlaylist(playlist))
return web.Response()

if type_ == "album":
assert isinstance(id_, str)
playlist = await SongListPlayerPlaylist.album(id_)
await playlist.waitForLoad()
asyncio.create_task(self._player.loadPlaylist(playlist))
return web.Response()

return web.HTTPBadRequest(text="invalid type")

@withObjectPayload(
Expand Down
3 changes: 1 addition & 2 deletions src/server/player/playlistManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,9 @@ async def _implement(song: Song) -> None:
playlists = self._playlists.values()
for playlist in playlists:
for song in playlist:
self._logger.debug("fetch album for %s", song)
if song.albumInDb:
self._logger.debug("song album is already in db %s", song)
continue
self._logger.debug("fetch album for %s", song)
await _implement(song)

async def addToPlaylist(self, playlistId: str, song: Song) -> bool:
Expand Down
12 changes: 12 additions & 0 deletions src/server/player/smartPlayerPlaylist.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,15 @@ async def artist(cls, artist: str) -> SongListPlayerPlaylist:
f"/artist/{artist}",
songs,
)

@classmethod
async def album(cls, albumHash: str) -> SongListPlayerPlaylist:
"""playlist from a single artist"""
songs = Song.list(await Database().songs.byAlbum(albumHash))
return cls(
"Album",
"Album",
albumHash,
f"/album/{albumHash}",
songs,
)
5 changes: 5 additions & 0 deletions src/server/router/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ def applyRoutes( # pylint: disable=too-many-statements, too-many-arguments
app.router.add_get("/api/artists/{name}", metaHandler.getArtist)
app.router.add_put("/api/artists/{name}", metaHandler.putArtist)

# /api/albums
app.router.add_get("/api/albums", metaHandler.getAlbums)
app.router.add_get("/api/albums/{albumHash}", metaHandler.getAlbum)
app.router.add_put("/api/albums/{albumHash}", metaHandler.putAlbum)

# /api/playlists/
app.router.add_get("/api/playlists/new", playlistHandler.createPlaylist)
app.router.add_get("/api/playlists", playlistHandler.getPlaylists)
Expand Down
1 change: 1 addition & 0 deletions src/ui/dist/assets/Album-1f347aa1.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/ui/dist/assets/Album-c485014b.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file added src/ui/dist/assets/Album-c485014b.js.gz
Binary file not shown.
1 change: 0 additions & 1 deletion src/ui/dist/assets/Albums-9b5db7b3.css

This file was deleted.

1 change: 1 addition & 0 deletions src/ui/dist/assets/Albums-c2cb712e.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.padding-20[data-v-8e421abc]{padding:20px}
1 change: 1 addition & 0 deletions src/ui/dist/assets/Albums-f18ee055.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion src/ui/dist/assets/Albums-fcb1e7f2.js

This file was deleted.

Loading

0 comments on commit 01af47a

Please sign in to comment.