mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
Skipping channel import during test unless explicitly requested. Deleting gis database during upgrade, if user agrees. Increasing API verbosity. Using status context to prevent fetching status in useHotspot. Disabling wifi toggle button when pending. Toggling hotspot when clicking nav hotspot icon. Displaying Hotspot status icon in nav menu. Importing channels/favorites after refreshing. Refresh tasks cancel when server shuts down. Bumping version for release speed. Refreshing files is 3x faster. Creating useRecurringTimeout hook which repeatedly calls a callback after a timeout. Changing intervals to timeout so fetches don't stack up while server is loaded. Checking that video file exists before fetching. Creating release candidate branch. Not indexing model associated files.
173 lines
6.4 KiB
Python
173 lines
6.4 KiB
Python
import json
|
|
import pathlib
|
|
from abc import ABC
|
|
from typing import Tuple, Dict, List
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from wrolpi.common import logger, register_modeler, register_after_refresh, limit_concurrent
|
|
from wrolpi.db import optional_session, get_db_session
|
|
from wrolpi.downloader import Downloader, Download, DownloadResult
|
|
from wrolpi.errors import UnrecoverableDownloadError
|
|
from wrolpi.files.indexers import register_indexer, Indexer
|
|
from wrolpi.files.models import File
|
|
from wrolpi.vars import PYTEST
|
|
from . import lib
|
|
from .api import bp # noqa
|
|
from .lib import match_archive_files
|
|
from .models import Archive, Domain
|
|
|
|
PRETTY_NAME = 'Archive'
|
|
|
|
logger = logger.getChild(__name__)
|
|
|
|
__all__ = ['ArchiveDownloader', 'archive_downloader']
|
|
|
|
|
|
class ArchiveDownloader(Downloader, ABC):
|
|
name = 'archive'
|
|
pretty_name = 'Archive'
|
|
|
|
def __repr__(self):
|
|
return f'<ArchiveDownloader>'
|
|
|
|
@classmethod
|
|
def valid_url(cls, url: str) -> Tuple[bool, None]:
|
|
"""
|
|
Archiver will attempt to archive anything, so it should be last!
|
|
"""
|
|
return True, None
|
|
|
|
async def do_download(self, download: Download) -> DownloadResult:
|
|
if download.attempts > 3:
|
|
raise UnrecoverableDownloadError(f'Max download attempts reached for {download.url}')
|
|
|
|
archive = await lib.do_archive(download.url)
|
|
return DownloadResult(success=True, location=f'/archive/{archive.id}')
|
|
|
|
@optional_session
|
|
def already_downloaded(self, *urls: List[str], session: Session = None) -> List:
|
|
archives = list(session.query(Archive).filter(Archive.url.in_(urls)))
|
|
return archives
|
|
|
|
|
|
# Archive downloader is the last downloader which should be used.
|
|
archive_downloader = ArchiveDownloader(priority=100)
|
|
|
|
|
|
@register_modeler
|
|
def archive_modeler(groups: Dict[str, List[File]], session: Session):
|
|
archive_groups = {}
|
|
for stem, group in groups.items():
|
|
if any(lib.is_singlefile_file(i.path) for i in group):
|
|
archive_groups[stem] = group
|
|
|
|
for stem, group in archive_groups.items():
|
|
readability_file, singlefile_file, readability_json_file, readability_txt_file, screenshot_file = \
|
|
match_archive_files(group)
|
|
|
|
archive: Archive = Archive.find_by_path(singlefile_file.path, session)
|
|
if not archive:
|
|
archive = Archive(singlefile_file=singlefile_file)
|
|
session.add(archive)
|
|
archive.readability_file = readability_file
|
|
archive.readability_json_file = readability_json_file
|
|
archive.readability_txt_file = readability_txt_file
|
|
archive.screenshot_file = screenshot_file
|
|
|
|
if archive.readability_file:
|
|
archive.readability_file.associated = True
|
|
archive.readability_file.do_stats()
|
|
if archive.readability_json_file:
|
|
archive.readability_json_file.associated = True
|
|
archive.readability_json_file.do_stats()
|
|
if archive.readability_txt_file:
|
|
archive.readability_txt_file.associated = True
|
|
archive.readability_txt_file.do_stats()
|
|
if archive.screenshot_file:
|
|
archive.screenshot_file.associated = True
|
|
archive.screenshot_file.do_stats()
|
|
|
|
archive.singlefile_file.model = Archive.__tablename__
|
|
|
|
archive.singlefile_file.do_index()
|
|
archive.validate()
|
|
|
|
# Remove this group, it will not be processed by other modelers.
|
|
del groups[stem]
|
|
|
|
|
|
@register_after_refresh
|
|
@limit_concurrent(1)
|
|
def archive_cleanup():
|
|
# Process all Archives that have not been validated. Delete any that no longer exist, or are not real Archives.
|
|
# Validate all other Archives.
|
|
logger.info('Searching for invalid Archives')
|
|
offset = 0
|
|
limit = 100
|
|
while True:
|
|
with get_db_session(commit=True) as session:
|
|
archives: List[Archive] = session.query(Archive).order_by(Archive.id).limit(limit).offset(offset)
|
|
processed = 0
|
|
deleted = 0
|
|
for archive in archives:
|
|
processed += 1
|
|
if not lib.is_singlefile_file(archive.singlefile_path):
|
|
# Archive is not a real archive.
|
|
archive.singlefile_file.model = None
|
|
session.delete(archive)
|
|
deleted += 1
|
|
if processed < limit:
|
|
# Validated all archives.
|
|
break
|
|
offset += (limit - deleted)
|
|
|
|
with get_db_session(commit=True) as session:
|
|
# Remove any Domains without any Archives.
|
|
domain_ids = [i[0] for i in session.execute('SELECT DISTINCT domain_id FROM archive') if i[0]]
|
|
for domain in session.query(Domain):
|
|
if domain.id not in domain_ids:
|
|
session.delete(domain)
|
|
|
|
|
|
@register_indexer('text/html')
|
|
class ArchiveIndexer(Indexer, ABC):
|
|
"""Gathers index data from the files associated with an Archive."""
|
|
|
|
@staticmethod
|
|
def get_title(file: File):
|
|
singlefile_path: pathlib.Path = file.path.path if hasattr(file.path, 'path') else file.path
|
|
readability_json_path = singlefile_path.with_suffix('.readability.json')
|
|
if readability_json_path.is_file():
|
|
with readability_json_path.open('rt') as fh:
|
|
try:
|
|
return json.load(fh)['title']
|
|
except Exception:
|
|
if not PYTEST:
|
|
logger.warning(f'Archive readability json file exists, but cannot get title. {file}')
|
|
return None
|
|
|
|
@staticmethod
|
|
def get_article(file: File):
|
|
singlefile_path: pathlib.Path = file.path.path if hasattr(file.path, 'path') else file.path
|
|
readability_txt_path = singlefile_path.with_suffix('.readability.txt')
|
|
if readability_txt_path.is_file():
|
|
return readability_txt_path.read_text()
|
|
|
|
@classmethod
|
|
def create_index(cls, file: File) -> Tuple:
|
|
"""
|
|
Index an Archive file and it's associated files. This mimetype matches non-archive (non-singlefile) html files,
|
|
so this index will return empty if a readability file is found.
|
|
|
|
a = title
|
|
b = <empty>
|
|
c = <empty>
|
|
d = readability article
|
|
"""
|
|
if file.path.name.endswith('.readability.html'):
|
|
# Readability files are not the primary Archive file.
|
|
return None, None, None, None
|
|
a = cls.get_title(file)
|
|
d = cls.get_article(file)
|
|
return a, None, None, d
|