Batch download songs; add tests for song invalidation and deletion

This commit is contained in:
Sumner Evans
2020-05-09 05:25:37 -06:00
parent e0449bccb3
commit 0e8d2ca68b
9 changed files with 355 additions and 123 deletions

View File

@@ -1,5 +1,3 @@
import hashlib
import json
import logging
import shutil
import threading
@@ -8,6 +6,7 @@ from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Optional, Sequence, Tuple
from sublime import util
from sublime.adapters import api_objects as API
from . import models
@@ -36,7 +35,10 @@ class FilesystemAdapter(CachingAdapter):
):
self.data_directory = data_directory
self.cover_art_dir = self.data_directory.joinpath("cover_art")
self.music_dir = self.data_directory.joinpath("music")
self.cover_art_dir.mkdir(parents=True, exist_ok=True)
self.music_dir.mkdir(parents=True, exist_ok=True)
self.is_cache = is_cache
@@ -64,17 +66,13 @@ class FilesystemAdapter(CachingAdapter):
# Data Helper Methods
# ==================================================================================
def _params_hash(self, *params: Any) -> str:
return hashlib.sha1(bytes(json.dumps(params), "utf8")).hexdigest()
# Data Retrieval Methods
# ==================================================================================
def get_cached_status(self, song: API.Song) -> SongCacheStatus:
# TODO: check if being downloaded
# TODO: change this path to be the correct dir
relative_path = models.Song.get_by_id(song.id).path
cache_path = self.data_directory.parent.joinpath(relative_path)
cache_path = self.music_dir.joinpath(relative_path)
if cache_path.exists():
# TODO check if path is permanently cached
return SongCacheStatus.CACHED
@@ -107,7 +105,7 @@ class FilesystemAdapter(CachingAdapter):
cache_key = CachingAdapter.CachedDataKey.PLAYLIST_DETAILS
cache_info = models.CacheInfo.get_or_none(
models.CacheInfo.cache_key == cache_key,
models.CacheInfo.params_hash == self._params_hash(playlist_id),
models.CacheInfo.params_hash == util.params_hash(playlist_id),
)
if not cache_info:
raise CacheMissError(partial_data=playlist)
@@ -115,13 +113,13 @@ class FilesystemAdapter(CachingAdapter):
return playlist
def get_cover_art_uri(self, cover_art_id: str, scheme: str) -> str:
params_hash = self._params_hash(cover_art_id)
params_hash = util.params_hash(cover_art_id)
cover_art_filename = self.cover_art_dir.joinpath(params_hash)
# Handle the case that this is the ground truth adapter.
if not self.is_cache:
if not cover_art_filename.exists:
raise Exception(f"Cover Art {cover_art_id} does not exist.")
raise Exception(f"Cover Art for {cover_art_id} does not exist.")
return str(cover_art_filename)
if not cover_art_filename.exists():
@@ -129,7 +127,8 @@ class FilesystemAdapter(CachingAdapter):
cache_key = CachingAdapter.CachedDataKey.COVER_ART_FILE
cache_info = models.CacheInfo.get_or_none(
models.CacheInfo.cache_key == cache_key, params_hash == params_hash
models.CacheInfo.cache_key == cache_key,
models.CacheInfo.params_hash == params_hash,
)
if not cache_info:
raise CacheMissError(partial_data=str(cover_art_filename))
@@ -137,7 +136,33 @@ class FilesystemAdapter(CachingAdapter):
return str(cover_art_filename)
def get_song_uri(self, song_id: str, scheme: str, stream=False) -> str:
raise CacheMissError()
song = models.Song.get_or_none(song_id)
if not song:
if self.is_cache:
raise CacheMissError()
else:
raise Exception(f"Song {song_id} does not exist.")
music_filename = self.music_dir.joinpath(song.path)
# Handle the case that this is the ground truth adapter.
if not self.is_cache:
if not music_filename.exists:
raise Exception(f"Music File for song {song_id} does not exist.")
return str(music_filename)
if not music_filename.exists():
raise CacheMissError()
cache_key = CachingAdapter.CachedDataKey.SONG_FILE
cache_info = models.CacheInfo.get_or_none(
models.CacheInfo.cache_key == cache_key,
models.CacheInfo.params_hash == util.params_hash(song_id),
)
if not cache_info:
raise CacheMissError(partial_data=str(music_filename))
return str(music_filename)
# Data Ingestion Methods
# ==================================================================================
@@ -180,7 +205,7 @@ class FilesystemAdapter(CachingAdapter):
params: Tuple[Any, ...],
data: Any,
):
params_hash = self._params_hash(*params)
params_hash = util.params_hash(*params)
models.CacheInfo.insert(
cache_key=data_key,
params_hash=params_hash,
@@ -227,13 +252,24 @@ class FilesystemAdapter(CachingAdapter):
elif data_key == CachingAdapter.CachedDataKey.COVER_ART_FILE:
# ``data`` is the filename of the tempfile in this case
shutil.copy(str(data), str(self.cover_art_dir.joinpath(params_hash)))
elif data_key == CachingAdapter.CachedDataKey.SONG_FILE:
relative_path = models.Song.get_by_id(params[0]).path
absolute_path = self.music_dir.joinpath(relative_path)
absolute_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(str(data), str(absolute_path))
def _invalidate_cover_art(self, cover_art_id: str):
models.CacheInfo.delete().where(
models.CacheInfo.cache_key == CachingAdapter.CachedDataKey.COVER_ART_FILE,
models.CacheInfo.params_hash == util.params_hash(cover_art_id),
).execute()
def _do_invalidate_data(
self, data_key: "CachingAdapter.CachedDataKey", params: Tuple[Any, ...],
):
models.CacheInfo.delete().where(
models.CacheInfo.cache_key == data_key,
models.CacheInfo.params_hash == self._params_hash(*params),
models.CacheInfo.params_hash == util.params_hash(*params),
).execute()
if data_key == CachingAdapter.CachedDataKey.PLAYLIST_DETAILS:
@@ -243,12 +279,16 @@ class FilesystemAdapter(CachingAdapter):
return
if playlist.cover_art:
cover_art_key = CachingAdapter.CachedDataKey.COVER_ART_FILE
cover_art_params_hash = self._params_hash(playlist.cover_art)
models.CacheInfo.delete().where(
models.CacheInfo.cache_key == cover_art_key,
models.CacheInfo.params_hash == cover_art_params_hash,
).execute()
self._invalidate_cover_art(playlist.cover_art)
elif data_key == CachingAdapter.CachedDataKey.SONG_FILE:
# Invalidate the corresponding cover art.
song = models.Song.get_or_none(models.Song.id == params[0])
if not song:
return
if song.cover_art:
self._invalidate_cover_art(song.cover_art)
def _do_delete_data(
self, data_key: "CachingAdapter.CachedDataKey", params: Tuple[Any, ...],
@@ -256,9 +296,15 @@ class FilesystemAdapter(CachingAdapter):
# Delete it from the cache info.
models.CacheInfo.delete().where(
models.CacheInfo.cache_key == data_key,
models.CacheInfo.params_hash == self._params_hash(*params),
models.CacheInfo.params_hash == util.params_hash(*params),
).execute()
def delete_cover_art(cover_art_filename):
cover_art_params_hash = util.params_hash(playlist.cover_art)
if cover_art_file := self.cover_art_dir.joinpath(cover_art_params_hash):
cover_art_file.unlink(missing_ok=True)
self._invalidate_cover_art(playlist.cover_art)
if data_key == CachingAdapter.CachedDataKey.PLAYLIST_DETAILS:
# Delete the playlist and corresponding cover art.
playlist = models.Playlist.get_or_none(models.Playlist.id == params[0])
@@ -266,13 +312,17 @@ class FilesystemAdapter(CachingAdapter):
return
if playlist.cover_art:
cover_art_params_hash = self._params_hash(playlist.cover_art)
if cover_art_file := self.cover_art_dir.joinpath(cover_art_params_hash):
cover_art_file.unlink(missing_ok=True)
cover_art_key = CachingAdapter.CachedDataKey.COVER_ART_FILE
models.CacheInfo.delete().where(
models.CacheInfo.cache_key == cover_art_key,
models.CacheInfo.params_hash == cover_art_params_hash,
).execute()
delete_cover_art(playlist.cover_art)
playlist.delete_instance()
elif data_key == CachingAdapter.CachedDataKey.SONG_FILE:
# Delete the song and corresponding cover art.
song = models.Song.get_or_none(models.Song.id == params[0])
if not song:
return
if song.cover_art:
delete_cover_art(song.cover_art)
song.delete_instance()