Batch download songs; add tests for song invalidation and deletion
This commit is contained in:
@@ -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()
|
||||
|
Reference in New Issue
Block a user