Compare commits

4 Commits

Author SHA1 Message Date
Bryan Bailey
9c38e4a6eb updated unit tests; implemented volume property and related tests 2022-06-22 23:19:34 -04:00
Bryan Bailey
e55cd132d4 Refactored comic.py. Removed ComicArchive class 2022-06-22 22:45:56 -04:00
Bryan Bailey
24ae5b9393 fixed missing installation dependency 2022-05-25 19:02:17 -04:00
Bryan Bailey
4b6a8cf821 Update issue templates 2022-05-25 16:09:52 -04:00
7 changed files with 127 additions and 70 deletions

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: Rigil-Kent
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
env
venv
__pycache__
.pytest_cache
.coverage

16
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,16 @@
{
"cSpell.words": [
"Archiver",
"dbsuper",
"draggable",
"errored",
"excaliber",
"mangadex",
"readallcomics",
"Scrapable",
"skippable",
"Uncategorized",
"worktree",
"yoink"
]
}

View File

@@ -28,5 +28,5 @@ setuptools.setup(
'yoink = yoink.cli:yoink'
]
},
install_requires=['click', 'bs4', 'requests']
install_requires=['click', 'bs4', 'requests', 'click-default-group']
)

View File

@@ -6,7 +6,7 @@ import click
from click_default_group import DefaultGroup
from yoink.config import YoinkConfig, app_root, config_from_file, library_path, config_path
from yoink.comic import Comic
from yoink.comic import Comic, download_comic_files, generate_archive, clean_up
@@ -22,13 +22,13 @@ def download_comic(url, series):
return 1
click.echo(f'Downloading {comic.title}')
comic.archiver.download()
download_comic_files(comic)
click.echo('Building comic archive')
comic.archiver.generate_archive()
generate_archive(comic)
click.echo('Cleaning up')
comic.archiver.cleanup_worktree()
clean_up(comic)
click.echo('Success')
@@ -50,8 +50,6 @@ def init():
@yoink.command()
# @click.option('-c', '--comic', is_flag=True, help='Download a Comic file')
# @click.option('-t', '--torrent', is_flag=True, help='Download a Torrent')
@click.option('-s', '--series', is_flag=True, help='Download the entire series')
@click.argument('url')
def download(url, series):

View File

@@ -1,5 +1,5 @@
from urllib.error import HTTPError
from yoink.config import required_archive_files, skippable_images, library_path, config
from yoink.config import required_archive_files, skippable_images, config
from yoink.scraper import Scrapable
import os
@@ -12,7 +12,6 @@ import re
class Comic(Scrapable):
def __init__(self, url, path=None) -> None:
super().__init__(url)
self.archiver = ComicArchiver(self, library=path)
def __is_supported_image(self, image):
return image.endswith('.jpg' or '.jpeg')
@@ -83,7 +82,14 @@ class Comic(Scrapable):
@property
def volume(self) -> int:
return
delimiter = ' v'
try:
if self.title.find(delimiter) and self.title[self.title.index(delimiter) + 2].isdigit():
return self.title[self.title.index(delimiter) + 2]
else:
return 1
except ValueError:
return 1
@property
def next(self) -> str:
@@ -106,70 +112,68 @@ class Comic(Scrapable):
return not filename.endswith(config.skippable_images)
class ComicArchiver:
def __init__(self, comic : Comic, library=None) -> None:
self.comic = comic
self.worktree = library if library else os.path.join(config.library_path, f'comics/{self.comic.title}')
self.queue = []
def add(self, link : str) -> None:
self.queue.append(link)
def download_comic_files(comic: Comic, worktree = None):
if not worktree:
worktree = os.path.join(config.library_path, f'comics/{comic.title}')
def download(self) -> None:
if not os.path.exists(worktree):
os.makedirs(worktree, mode=0o777)
if not os.path.exists(self.worktree):
os.makedirs(self.worktree, mode=0o777)
opener = urllib.request.build_opener()
opener.addheaders = [('User-Agent', "Mozilla/5.0")]
urllib.request.install_opener(opener)
opener = urllib.request.build_opener()
opener.addheaders = [('User-Agent', 'Mozilla/5.0')]
urllib.request.install_opener(opener)
try:
for index,url in enumerate(comic.filelist):
if not url.endswith('.jpg'):
formatted_file = os.path.join(worktree, f'{comic.title} ' + ''.join([str(index).zfill(3), '.jpg']))
print(formatted_file, end='\r')
urllib.request.urlretrieve(url, filename=formatted_file)
else:
page_number = str(index).zfill(3)
file_extension = url.split('/')[-1].split('.')[1]
try:
for index,url in enumerate(self.comic.filelist):
if len(file_extension) > 3:
file_extension = 'jpg'
if not url.endswith('.jpg'):
formatted_file = os.path.join(self.worktree, f'{self.comic.title} ' + ''.join([str(index).zfill(3), '.jpg']))
print(formatted_file, end='\r')
urllib.request.urlretrieve(url, filename=formatted_file)
else:
page_number = str(index).zfill(3)
file_extension = url.split('/')[-1].split('.')[1]
formatted_file = f'{comic.title} - {page_number}.{file_extension}'
print(formatted_file, end='\r',)
urllib.request.urlretrieve(url, filename=os.path.join(worktree, formatted_file))
except HTTPError:
# the page itself loads but the images (stored on another server) 4040
raise ReferenceError(f'Issue {comic.title} #{comic.issue_number} could not be found. The page may be down or the images might have errored: {self.comic.url}')
if len(file_extension) > 3:
file_extension = 'jpg'
def generate_archive(comic: Comic, worktree = None, archive_format = '.cbr'):
if not worktree:
worktree = os.path.join(config.library_path, f'comics/{comic.title}')
formatted_file = f'{self.comic.title} - {page_number}.{file_extension}'
print(formatted_file, end='\r',)
urllib.request.urlretrieve(url, filename=os.path.join(self.worktree, formatted_file))
except HTTPError:
# the page itself loads but the images (stored on another server) 4040
raise ReferenceError(f'Issue {self.comic.title} #{self.comic.issue_number} could not be found. The page may be down or the images might have errored: {self.comic.url}')
archive_from = os.path.basename(worktree)
if os.path.exists(os.path.join(worktree, f'{comic.title}{archive_format}')):
return
print()
output = shutil.make_archive(comic.title, 'zip', worktree, worktree)
# os.rename causes OSError: [Errno 18] Invalid cross-device link and files build test
# os rename only works if src and dest are on the same file system
shutil.move(output, os.path.join(worktree, f'{comic.title}{archive_format}'))
def generate_archive(self, archive_format='.cbr'):
def clean_up(comic: Comic):
worktree = os.path.join(config.library_path, f'comics/{comic.title}')
archive_from = os.path.basename(self.worktree)
if os.path.exists(os.path.join(self.worktree, f'{self.comic.title}{archive_format}')):
return
output = shutil.make_archive(self.comic.title, 'zip', self.worktree, self.worktree)
# os.rename casuses OSError: [Errno 18] Invalid cross-device link and files build test
# os rename only works if src and dest are on the same file system
shutil.move(output, os.path.join(self.worktree, f'{self.comic.title}{archive_format}'))
def cleanup_worktree(self):
for image in os.listdir(self.worktree):
for image in os.listdir(worktree):
if not image.endswith(required_archive_files):
os.remove(os.path.join(self.worktree, image))
os.remove(os.path.join(worktree, image))
if __name__ == '__main__':
comic = Comic('http://www.readallcomics.com/static-season-one-4-2021/') # all links
# comic = Comic('http://www.readallcomics.com/static-season-one-4-2021/') # all links
comic = Comic('http://readallcomics.com/x-men-v6-12-2022/')
# comic = Comic('http://readallcomics.com/static-season-one-001-2021/') # no prev link
# comic = Comic('http://readallcomics.com/static-season-one-6-2022/') # no next link
# comic = Comic('http://readallcomics.com/superman-vs-lobo-4-2022/')
# test_comic_b = 'http://readallcomics.com/captain-marvel-vs-rogue-2021-part-1/'
# comic = Comic('http://readallcomics.com/captain-marvel-vs-rogue-2021-part-1/')
print(comic.volume)
# print(comic.next)
# print(comic.prev)
# print(comic.issue_number)

View File

@@ -5,7 +5,7 @@ import unittest
from shutil import rmtree
from yoink.config import app_root, library_path, config_path, skippable_images, supported_sites, required_archive_files
from yoink.comic import Comic, ComicArchiver
from yoink.comic import Comic, download_comic_files, generate_archive, clean_up
from yoink.scraper import Scrapable
@@ -15,13 +15,13 @@ class BasicTestCase(unittest.TestCase):
self.test_comic = 'http://readallcomics.com/static-season-one-6-2022/'
self.test_comic_b = 'http://readallcomics.com/captain-marvel-vs-rogue-2021-part-1/'
self.comic = Comic(self.test_comic)
self.archiver = ComicArchiver(self.comic)
self.remove_queue = []
self.expected_title = 'Static Season One 6 (2022)'
self.expected_title_b = 'Captain Marvel vs. Rogue (2021 Part 1)'
self.expected_category = 'Static: Season One'
self.expected_category_b = 'Captain Marvel vs. Rogue'
self.expected_issue_num = 6
self.expected_vol_num = 1
self.expected_next_url = None
self.expected_prev_url = 'http://readallcomics.com/static-season-one-5-2022/'
self.erroneous_comic = 'http://readallcomics.com/static-season-one-4-2021/'
@@ -39,28 +39,28 @@ class BasicTestCase(unittest.TestCase):
def test_001_comic_has_valid_title(self):
self.assertEqual(self.expected_title, self.comic.title)
def test_002_comic_has_valid_category(self):
def test_002_comic_has_valid_volume_numer(self):
self.assertEqual(self.expected_vol_num, self.comic.volume)
def test_003_comic_has_valid_category(self):
self.assertEqual(self.expected_category, self.comic.category)
def test_003_empty_comic_folder(self):
def test_004_empty_comic_folder(self):
self.assertEqual(len(os.listdir(os.path.join(library_path, 'comics'))), 0)
def test_004_comic_folder_created_and_populated(self):
self.archiver.download()
def test_005_comic_folder_created_and_populated(self):
download_comic_files(self.comic)
self.assertTrue(os.path.exists(os.path.join(library_path, f'comics/{self.comic.title}')))
self.assertGreater(len(os.listdir(os.path.join(library_path, f'comics/{self.comic.title}'))), 0)
def test_005_comic_archive_generated(self):
self.archiver.generate_archive()
def test_006_comic_archive_generated(self):
generate_archive(self.comic)
self.assertTrue(os.path.exists(os.path.join(library_path, f'comics/{self.comic.title}/{self.comic.title}.cbr')))
def test_006_folder_cleaned_after_archive_generation(self):
self.archiver.cleanup_worktree()
def test_007_folder_cleaned_after_archive_generation(self):
clean_up(self.comic)
self.assertLessEqual(len(os.listdir(os.path.join(library_path, f'comics/{self.comic.title}'))), 3)
def test_007_comic_instance_has_archiver(self):
self.assertIsInstance(self.comic.archiver, ComicArchiver)
def test_008_comic_is_subclass_scrapable(self):
self.assertTrue(issubclass(Comic, Scrapable))