WIP adding CRUD operations for playlists

This commit is contained in:
evilchili 2022-11-24 13:45:09 -08:00
parent 54d6915ed7
commit 5f8ab8fe25
6 changed files with 193 additions and 55 deletions

View File

@ -3,15 +3,20 @@ import os
import typer import typer
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional, List
from dotenv import load_dotenv from dotenv import load_dotenv
from slugify import slugify
from pprint import pprint
from groove import webserver from groove import webserver
from groove.playlist import Playlist
from groove.db.manager import database_manager from groove.db.manager import database_manager
from groove.db.scanner import media_scanner from groove.db.scanner import media_scanner
playlist_app = typer.Typer()
app = typer.Typer() app = typer.Typer()
app.add_typer(playlist_app, name='playlist', help='Manage playlists.')
def initialize(): def initialize():
@ -21,6 +26,68 @@ def initialize():
level=logging.DEBUG if debug else logging.INFO) level=logging.DEBUG if debug else logging.INFO)
@playlist_app.command()
def delete(
name: str = typer.Argument(
...,
help="The name of the playlist to create."
),
no_dry_run: bool = typer.Option(
False,
help="If True, actually delete the playlist, Otherwise, show what would be deleted."
)
):
"""
Delete a playlist
"""
initialize()
with database_manager() as manager:
pl = Playlist(slug=slugify(name), connection=manager.session, create_if_not_exists=False)
if not pl.exists:
logging.info(f"No playlist named '{name}' could be found.")
return
if no_dry_run is False:
print(f"Would delete playlist {pl.record.id}, which contains {len(pl.entries)} tracks.")
return
deleted_playlist = pl.delete()
print(f"Playlist {deleted_playlist} deleted.")
@playlist_app.command()
def add(
name: str = typer.Argument(
...,
help="The name of the playlist to create."
),
tracks: List[str] = typer.Option(
None,
help="A list of tracks to add to the playlist."
),
exists_ok: bool = typer.Option(
True,
help="If True, it is okay if the playlist already exists."
),
multiples_ok: bool = typer.Option(
False,
help="If True, the same track can be added to the playlist multiple times."
)
):
"""
Create a new playlist with the specified name, unless it already exists.
"""
initialize()
with database_manager() as manager:
pl = Playlist(slug=slugify(name), connection=manager.session, create_if_not_exists=True)
if pl.exists:
if not exists_ok:
raise RuntimeError(f"Playlist with slug {pl.slug} already exists!")
logging.debug(pl.as_dict)
if tracks:
pl.add(tracks)
pprint(pl.as_dict)
@app.command() @app.command()
def scan( def scan(
root: Optional[Path] = typer.Option( root: Optional[Path] = typer.Option(
@ -41,6 +108,7 @@ def scan(
count = scanner.scan() count = scanner.scan()
logging.info(f"Imported {count} new tracks from {root}.") logging.info(f"Imported {count} new tracks from {root}.")
@app.command() @app.command()
def server( def server(
host: str = typer.Argument( host: str = typer.Argument(

View File

@ -1,49 +0,0 @@
import json
import logging
from bottle import HTTPResponse
from sqlalchemy import bindparam
from groove import db
class PlaylistDatabaseHelper:
"""
Convenience class for database interactions.
"""
def __init__(self, connection):
self._conn = connection
@property
def conn(self):
return self._conn
def playlist(self, slug: str) -> dict:
"""
Retrieve a playlist and its entries by its slug.
"""
playlist = {}
query = db.playlist.select(db.playlist.c.slug == bindparam('slug'))
logging.debug(f"playlist: '{slug}' requested. Query: {query}")
results = self.conn.execute(str(query), {'slug': slug}).fetchone()
if not results:
return playlist
playlist = results
query = db.entry.select(db.entry.c.playlist_id == bindparam('playlist_id'))
logging.debug(f"Retrieving playlist entries. Query: {query}")
entries = self.conn.execute(str(query), {'playlist_id': playlist['id']}).fetchall()
playlist = dict(playlist)
playlist['entries'] = [dict(entry) for entry in entries]
return playlist
def json_response(self, playlist: dict, status: int = 200) -> HTTPResponse:
"""
Create an application/json HTTPResponse object out of a playlist and its entries.
"""
response = json.dumps(playlist)
logging.debug(response)
return HTTPResponse(status=status, content_type='application/json', body=response)

104
groove/playlist.py Normal file
View File

@ -0,0 +1,104 @@
from groove import db
from sqlalchemy import func, delete
from sqlalchemy.exc import NoResultFound
import logging
class Playlist:
"""
CRUD operations and convenience methods for playlists.
"""
def __init__(self, slug, connection, create_if_not_exists=False):
self._conn = connection
self._slug = slug
self._record = None
self._entries = None
self._create_if_not_exists = create_if_not_exists
@property
def exists(self):
return self.record is not None
@property
def slug(self):
return self._slug
@property
def conn(self):
return self._conn
@property
def record(self):
if not self._record:
try:
self._record = self.conn.query(db.playlist).filter(db.playlist.c.slug == self.slug).one()
logging.debug(f"Retrieved playlist {self._record.id}")
except NoResultFound:
pass
if self._create_if_not_exists:
self._record = self._create()
if not self._record:
raise RuntimeError(f"Tried to create a playlist but couldn't read it back using slug {self.slug}")
return self._record
@property
def entries(self):
if not self._entries:
self._entries = self.conn.query(
db.entry,
db.track
).filter(
(db.playlist.c.id == self.record.id)
).filter(
db.entry.c.playlist_id == db.playlist.c.id
).filter(
db.entry.c.track_id == db.track.c.id
).all()
return self._entries
@property
def as_dict(self) -> dict:
"""
Retrieve a playlist and its entries by its slug.
"""
playlist = {}
playlist = dict(self.record)
playlist['entries'] = [dict(entry) for entry in self.entries]
return playlist
def add(self, paths) -> int:
return self._create_entries(self._get_tracks_by_path(paths))
def delete(self):
plid = self.record.id
stmt = delete(db.entry).where(db.entry.c.playlist_id == plid)
logging.debug(f"Deleting entries associated with playlist {plid}: {stmt}")
self.conn.execute(stmt)
stmt = delete(db.playlist).where(db.playlist.c.id == plid)
logging.debug(f"Deleting playlist {plid}: {stmt}")
self.conn.execute(stmt)
self.conn.commit()
return plid
def _get_tracks_by_path(self, paths):
return [self.conn.query(db.track).filter(db.track.c.relpath.ilike(f"%{path}%")).one() for path in paths]
def _create_entries(self, tracks):
maxtrack = self.conn.query(func.max(db.entry.c.track)).filter_by(playlist_id=self.record.id).one()[0]
self.conn.execute(
db.entry.insert(),
[
{'playlist_id': self.record.id, 'track_id': obj.id, 'track': idx}
for (idx, obj) in enumerate(tracks, start=maxtrack+1)
]
)
self.conn.commit()
return len(tracks)
def _create(self):
stmt = db.playlist.insert({'slug': self.slug})
results = self.conn.execute(stmt)
self.conn.commit()
logging.debug(f"Created new playlist {results.inserted_primary_key} with slug {self.slug}")
return self.conn.query(db.playlist).filter(db.playlist.c.id == results.inserted_primary_key).one()

View File

@ -1,3 +1,4 @@
import json
import logging import logging
import os import os
@ -6,7 +7,7 @@ from bottle import HTTPResponse
from bottle.ext import sqlite from bottle.ext import sqlite
from groove.auth import is_authenticated from groove.auth import is_authenticated
from groove.helper import PlaylistDatabaseHelper from groove.playlist import Playlist
server = bottle.Bottle() server = bottle.Bottle()
@ -44,8 +45,9 @@ def get_playlist(slug, db):
Retrieve a playlist and its entries by a slug. Retrieve a playlist and its entries by a slug.
""" """
logging.debug(f"Looking up playlist: {slug}...") logging.debug(f"Looking up playlist: {slug}...")
pldb = PlaylistDatabaseHelper(connection=db) playlist = Playlist(slug=slug, conn=db)
playlist = pldb.playlist(slug) if not playlist.exists:
if not playlist:
return HTTPResponse(status=404, body="Not found") return HTTPResponse(status=404, body="Not found")
return pldb.json_response(playlist) response = json.dumps(playlist.as_dict)
logging.debug(response)
return HTTPResponse(status=200, content_type='application/json', body=response)

View File

@ -16,6 +16,7 @@ python-dotenv = "^0.21.0"
Paste = "^3.5.2" Paste = "^3.5.2"
bottle-sqlite = "^0.2.0" bottle-sqlite = "^0.2.0"
SQLAlchemy = "^1.4.44" SQLAlchemy = "^1.4.44"
python-slugify = "^7.0.0"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pytest = "^7.2.0" pytest = "^7.2.0"

12
test/test_playlists.py Normal file
View File

@ -0,0 +1,12 @@
def test_create_playlist():
pass
def test_update_playlist():
pass
def delete_playlist():
pass