def delete_associations( id_or_url: str, novel_service: BaseNovelService = Provide[ Application.services.novel_service], path_service: BasePathService = Provide[Application.services.path_service], asset_service: BaseAssetService = Provide[ Application.services.asset_service], ): """Removes all except vital information related to novel, this includes chapters, metadata, and assets.""" try: novel = cli_helpers.get_novel(id_or_url) except ValueError: sys.exit(1) logger.info(f"Removing associated data from '{novel.title}' ({novel.id})…") novel_service.delete_volumes(novel) logger.info("Deleted volumes and chapters of novel.") novel_service.delete_metadata(novel) logger.info("Deleted metadata of novel.") asset_service.delete_assets_of_novel(novel) logger.info("Deleted asset entries of novel.") novel_dir = path_service.novel_data_path(novel) if novel_dir.exists(): shutil.rmtree(novel_dir) logger.info( f"Deleted saved file data of novel: {{data.dir}}/{path_service.relative_to_data_dir(novel_dir)}." )
def get_novel( id_or_url: str, silent: bool = False, novel_service: BaseNovelService = Provide[ Application.services.novel_service], ) -> Novel: """retrieve novel is it exists in the database otherwise return none :raises ValueError: if novel does not exist """ is_url = url_helper.is_url(id_or_url) if is_url: novel = novel_service.get_novel_by_url(id_or_url) else: try: novel = novel_service.get_novel_by_id(int(id_or_url)) except ValueError: logger.error( f"Value provided is neither a url or an id: {id_or_url}.") sys.exit(1) if not novel: quote = "'" if is_url else "" msg = f"Novel not found: {quote}{id_or_url}{quote}." logger.info(msg) raise ValueError(msg) if not silent: logger.info(f"Acquired '{novel.title}' ({novel.id}) from database.") return novel
def delete_downloaded_content( id_or_url: str, novel_service: BaseNovelService = Provide[ Application.services.novel_service], ): """deletes all the downloaded content from chapters of novel""" try: novel = cli_helpers.get_novel(id_or_url) except ValueError: sys.exit(1) novel_service.delete_content(novel) logger.info(f"Deleted chapter content from '{novel.title}' ({novel.id}).")
def import_metadata( id_or_url: str, metadata_url: str, novel_service: BaseNovelService = Provide[ Application.services.novel_service], ): """import metadata from a metadata supplied into an existing novel""" try: novel = cli_helpers.get_novel(id_or_url) except ValueError: sys.exit(1) meta_source_gateway = cli_helpers.get_meta_source_gateway(metadata_url) metadata_dtos = meta_source_gateway.metadata_by_url(metadata_url) novel_service.update_metadata(novel, metadata_dtos)
def remove_url( url: str, novel_service: BaseNovelService = Provide[ Application.services.novel_service], ): """Removes the selected url from the database""" try: novel = get_novel(url) except ValueError: sys.exit(1) try: novel_service.remove_url(novel, url) except ValueError as e: logger.error(e) sys.exit(1) else: logger.info(f"Removed '{url}' from '{novel.title}' ({novel.id}).")
def add_url( id_or_url: str, new_url: str, novel_service: BaseNovelService = Provide[ Application.services.novel_service], ): """Deletes the novel and all its data""" try: novel = get_novel(id_or_url) except ValueError: sys.exit(1) try: novel_service.add_url(novel, new_url) except ValueError as e: logger.error(e) sys.exit(1) else: logger.info(f"Added '{new_url}' to '{novel.title}' ({novel.id}).")
def download_thumbnail( novel: Novel, force: bool = False, novel_service: BaseNovelService = Provide[ Application.services.novel_service], file_service: BaseFileService = Provide[Application.services.file_service], path_service: BasePathService = Provide[Application.services.path_service], ): thumbnail_path = path_service.thumbnail_path(novel) novel_service.set_thumbnail_asset( novel, path_service.relative_to_data_dir(thumbnail_path)) if not force and thumbnail_path.exists() and thumbnail_path.is_file(): logger.info("Skipped thumbnail download since file already exists.") return logger.debug( f"Attempting to download thumbnail from {novel.thumbnail_url}.") try: response = requests.get(novel.thumbnail_url) except requests.ConnectionError: raise NSError( "Connection terminated unexpectedly; Make sure you are connected to the internet." ) if not response.ok: logger.error( f"Encountered an error during thumbnail download: {response.status_code} {response.reason}." ) return thumbnail_path.parent.mkdir(parents=True, exist_ok=True) file_service.write_bytes(thumbnail_path, response.content) size = string_helper.format_bytes(len(response.content)) logger.info( f"Downloaded and saved thumbnail image to {novel.thumbnail_path} ({size})." )
def list_novels( novel_service: BaseNovelService = Provide[ Application.services.novel_service], source_service: BaseSourceService = Provide[ Application.services.source_service], ): novels = novel_service.get_all_novels() table = [["Id", "Title", "Source", "Last updated"]] for novel in novels: url = novel_service.get_primary_url(novel) try: source = source_service.source_from_url(url).name except SourceNotFoundException: source = None table.append([novel.id, novel.title, source, novel.last_updated]) for line in tabulate(table, headers="firstrow", tablefmt="github").splitlines(): logger.info(line)
def delete_novel( id_or_url: str, novel_service: BaseNovelService = Provide[ Application.services.novel_service], path_service: BasePathService = Provide[Application.services.path_service], ): """delete all records of novel. this includes chapters, and assets""" try: novel = cli_helpers.get_novel(id_or_url) except ValueError: sys.exit(1) logger.info(f"Deleting '{novel.title}' ({novel.id})…") novel_dir = path_service.novel_data_path(novel) if novel_dir.exists(): shutil.rmtree(novel_dir) logger.info( f"Deleted data of novel: {{data.dir}}/{path_service.relative_to_data_dir(novel_dir)}." ) novel_service.delete_novel(novel) logger.info("Deleted novel entry.")
def update_novel( novel: Novel, browser: Optional[str], novel_service: BaseNovelService = Provide[ Application.services.novel_service], ): url = novel_service.get_primary_url(novel) logger.debug(f"Acquired primary novel url: {url}.") source_gateway = get_source_gateway(url) novel_dto = retrieve_novel_info(source_gateway, url, browser) novel_service.update_novel(novel, novel_dto) novel_service.update_chapters(novel, novel_dto.volumes) novel_service.update_metadata(novel, novel_dto.metadata) chapters = [c for v in novel_dto.volumes for c in v.chapters] logger.info( f"Novel updated using new values: id={novel.id} title='{novel.title}' chapters={len(chapters)}" ) return novel
def create_novel( url: str, browser: Optional[str], novel_service: BaseNovelService = Provide[ Application.services.novel_service], path_service: BasePathService = Provide[Application.services.path_service], ) -> Novel: """ retrieve information about the novel from webpage and insert novel into database. this includes chapter list and metadata. """ source_gateway = get_source_gateway(url) novel_dto = retrieve_novel_info(source_gateway, url, browser) novel = novel_service.insert_novel(novel_dto) try: novel_service.add_url(novel, url) except ValueError as e: logger.debug("(ERROR) " + str(e)) novel_service.insert_chapters(novel, novel_dto.volumes) novel_service.insert_metadata(novel, novel_dto.metadata) chapters = [c for v in novel_dto.volumes for c in v.chapters] logger.info( f"Added new novel with values: id={novel.id} title='{novel.title}' chapters={len(chapters)}." ) data_dir = path_service.novel_data_path(novel) if data_dir.exists(): logger.debug( f"Removing existing data in novel data dir: {{data.dir}}/{path_service.relative_to_data_dir(data_dir)}." ) shutil.rmtree(data_dir) return novel
def download_chapters( novel: Novel, limit: Optional[int], threads: Optional[int], novel_service: BaseNovelService = Provide[ Application.services.novel_service], asset_service: BaseAssetService = Provide[ Application.services.asset_service], dto_adapter: DTOAdapter = Provide[Application.adapters.dto_adapter], ): chapters = novel_service.get_pending_chapters(novel, limit) if not chapters: logger.info("Skipped chapter download as none are pending.") return url = novel_service.get_primary_url(novel) logger.debug(f"Acquired primary novel url: {url}.") source_gateway = get_source_gateway(url) thread_count = (min(threads, os.cpu_count()) if threads is not None else os.cpu_count()) def download(dto: ChapterDTO): try: return source_gateway.update_chapter_content(dto) except Exception as exc: raise ContentUpdateFailedException(dto, exc) logger.info( f"Downloading {len(chapters)} pending chapters with {thread_count} threads…" ) successes = 0 with tqdm(total=len(chapters), **TQDM_CONFIG) as pbar: with futures.ThreadPoolExecutor(max_workers=thread_count) as executor: download_futures = [ executor.submit(download, dto_adapter.chapter_to_dto(c)) for c in chapters ] for future in futures.as_completed(download_futures): try: chapter_dto = future.result() chapter_dto.content = asset_service.collect_assets( novel, chapter_dto) novel_service.update_content(chapter_dto) logger.debug( f"Chapter content downloaded: '{chapter_dto.title}' ({chapter_dto.index})" ) successes += 1 except ContentUpdateFailedException as e: logger.error( f"An error occurred during content download: {type(e.exception)}." ) logger.debug( "An error occurred during content download: {}", type(e.exception), ) pbar.update(1) logger.info( f"Chapters download complete, {successes} succeeded, with {len(chapters) - successes} errors." )
def show_info( id_or_url: str, fmt: Literal["default", "json"] = "default", novel_service: BaseNovelService = Provide[ Application.services.novel_service], ): """print current information of novel""" try: novel = cli_helpers.get_novel(id_or_url, silent=True) except ValueError: sys.exit(1) chapters = novel_service.get_chapters(novel) data = { "novel": { "id": novel.id, "title": novel.title, "author": novel.author, "lang": novel.lang, "thumbnail": novel.thumbnail_url, "synopsis": novel.synopsis.splitlines(), "urls": [o.url for o in novel_service.get_urls(novel)], }, "chapters": { "total": len(chapters), "downloaded": len([c for c in chapters if c.content]), }, } if fmt is None or fmt == "default": endl = "\n" text = "[novel]" + endl def format_keyvalue(key, value): text = f"{key} = " if type(value) == str: text += f"'{value}'" else: text += str(value) text += endl return text for key, value in data["novel"].items(): text += format_keyvalue(key, value) text += endl text += "[chapters]" + endl for key, value in data["chapters"].items(): text += format_keyvalue(key, value) elif fmt == "json": text = json.dumps(data, indent=4) else: raise NSError( f"Provided novel information formatter is not supported: {fmt}.") for line in text.splitlines(): logger.info(line)