mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
Removing default download freqeuncy so channels will not be downloaded without a url and frequency. Adding channel unique url constraint. Handling when a Channel's directory may still be relative in DB. Adding TODOs to remove old favorites. Removing outdated favorites from config after favorites have been created. Testing that old-style favorited videos in the config are imported. Removing channels_by_link. Using list to store channels in config. Moving channel import function into lib. Supporting old channel import with links. Bumping version for channel.link removal. Removing all reference to Channel.link in app and api. Adding relative property to MediaPathType. Testing that a channel can be downloaded after it has a frequency added. All tests pass. Video and Channel tests pass without channel.link. Removing channel.link usage in channel api. Changing video Statistics page to About.
179 lines
5.6 KiB
Python
179 lines
5.6 KiB
Python
import asyncio
|
|
import re
|
|
from functools import wraps
|
|
from pathlib import Path
|
|
from typing import List
|
|
from uuid import uuid4
|
|
|
|
import psycopg2
|
|
from sqlalchemy.orm import Session
|
|
|
|
from modules.files.models import File
|
|
from wrolpi.common import get_media_directory, wrol_mode_check, walk, chunks, logger
|
|
from wrolpi.dates import from_timestamp
|
|
from wrolpi.db import get_db_session, get_db_curs, get_ranked_models
|
|
from wrolpi.errors import InvalidFile
|
|
from wrolpi.vars import PYTEST
|
|
|
|
try:
|
|
import magic
|
|
|
|
have_magic = True
|
|
except ImportError:
|
|
logger.error('Could not import magic. Is libmagic installed?')
|
|
have_magic = False
|
|
|
|
logger = logger.getChild(__name__)
|
|
|
|
__all__ = ['list_files', 'delete_file', 'split_file_name', 'upsert_file', 'refresh_files', 'search']
|
|
|
|
|
|
def filter_parent_directories(directories: List[Path]) -> List[Path]:
|
|
"""
|
|
Remove parent directories if their children are in the list.
|
|
|
|
>>> filter_parent_directories([Path('foo'), Path('foo/bar'), Path('baz')])
|
|
[Path('foo/bar'), Path('baz')]
|
|
"""
|
|
unique_children = set()
|
|
for directory in sorted(directories):
|
|
for parent in directory.parents:
|
|
# Remove any parent of this child.
|
|
if parent in unique_children:
|
|
unique_children.remove(parent)
|
|
unique_children.add(directory)
|
|
|
|
# Restore the original order.
|
|
new_directories = [i for i in directories if i in unique_children]
|
|
return new_directories
|
|
|
|
|
|
def list_files(directories: List[str]) -> List[Path]:
|
|
"""
|
|
List all files down to the directories provided. This includes all parent directories of the directories.
|
|
"""
|
|
media_directory = get_media_directory()
|
|
|
|
# Always display the media_directory files.
|
|
paths = list(media_directory.iterdir())
|
|
|
|
if directories:
|
|
directories = [media_directory / i for i in directories if i]
|
|
directories = filter_parent_directories(directories)
|
|
for directory in directories:
|
|
directory = media_directory / directory
|
|
for parent in directory.parents:
|
|
is_relative_to = str(media_directory).startswith(str(parent))
|
|
if parent == Path('.') or is_relative_to:
|
|
continue
|
|
paths.extend(parent.iterdir())
|
|
paths.extend(directory.iterdir())
|
|
|
|
return paths
|
|
|
|
|
|
@wrol_mode_check
|
|
def delete_file(file: str):
|
|
"""Delete a file in the media directory."""
|
|
file = get_media_directory() / file
|
|
if file.is_dir() or not file.is_file():
|
|
raise InvalidFile(f'Invalid file {file}')
|
|
file.unlink()
|
|
|
|
|
|
FILE_NAME_REGEX = re.compile(r'[_ ]')
|
|
|
|
|
|
def split_file_name(path: Path) -> List[str]:
|
|
"""
|
|
Split a file name into words.
|
|
|
|
>>> split_file_name(Path('foo.mp4'))
|
|
['foo']
|
|
>>> split_file_name(Path('foo bar.mp4'))
|
|
['foo', 'bar']
|
|
"""
|
|
words = [str(i) for i in FILE_NAME_REGEX.split(path.stem)]
|
|
return words
|
|
|
|
|
|
def upsert_file(path: Path, session: Session) -> File:
|
|
"""Update/insert a File in the DB. Gather metadata about it."""
|
|
file = session.query(File).filter_by(path=path).one_or_none()
|
|
if not file:
|
|
file = File(path=path)
|
|
session.add(file)
|
|
if not file.mimetype and have_magic:
|
|
file.mimetype = magic.from_file(str(path), mime=True)
|
|
if not file.title:
|
|
file.title = split_file_name(path)
|
|
if not file.size or not file.modification_datetime:
|
|
stat = path.stat()
|
|
file.size = stat.st_size
|
|
file.modification_datetime = file.modification_datetime or from_timestamp(stat.st_mtime)
|
|
|
|
return file
|
|
|
|
|
|
def _refresh_files():
|
|
"""Find and index all files"""
|
|
logger.info('Refreshing Files')
|
|
# Mark all files as unverified. Any records that are unverified after refresh will be deleted.
|
|
with get_db_curs(commit=True) as curs:
|
|
curs.execute('UPDATE file SET idempotency=null')
|
|
|
|
paths = filter(lambda i: i.is_file(), walk(get_media_directory()))
|
|
idempotency = str(uuid4())
|
|
for chunk in chunks(paths, 20):
|
|
with get_db_session(commit=True) as session:
|
|
for path in chunk:
|
|
file = upsert_file(path, session)
|
|
file.idempotency = idempotency
|
|
|
|
# Remove any records where the file no longer exists.
|
|
with get_db_curs(commit=True) as curs:
|
|
curs.execute('DELETE FROM file WHERE idempotency IS NULL')
|
|
|
|
logger.info('Done refreshing Files')
|
|
|
|
|
|
@wraps(_refresh_files)
|
|
def refresh_files():
|
|
"""Schedule a refresh task if not testing. If testing, do a synchronous refresh."""
|
|
if PYTEST:
|
|
return _refresh_files()
|
|
|
|
async def _():
|
|
return _refresh_files()
|
|
|
|
asyncio.create_task(_())
|
|
|
|
|
|
def search(search_str: str, limit: int, offset: int):
|
|
"""
|
|
Search the "title" of each file. The title is the words in the file's stem.
|
|
"""
|
|
with get_db_curs() as curs:
|
|
stmt = '''
|
|
SELECT id, ts_rank_cd(textsearch, websearch_to_tsquery(%(search_str)s)), COUNT(*) OVER() AS total
|
|
FROM file
|
|
WHERE textsearch @@ websearch_to_tsquery(%(search_str)s)
|
|
OFFSET %(offset)s LIMIT %(limit)s
|
|
'''
|
|
params = dict(search_str=search_str, offset=offset, limit=limit)
|
|
curs.execute(stmt, params)
|
|
|
|
try:
|
|
results = list(map(dict, curs.fetchall()))
|
|
except psycopg2.ProgrammingError:
|
|
# No files.
|
|
return [], 0
|
|
|
|
total = results[0]['total'] if results else 0
|
|
ranked_ids = [i['id'] for i in results]
|
|
|
|
with get_db_session() as session:
|
|
results = get_ranked_models(ranked_ids, File, session)
|
|
results = [i.__json__() for i in results]
|
|
|
|
return results, total
|