Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c38e4a6eb | ||
|
|
e55cd132d4 | ||
|
|
24ae5b9393 | ||
|
|
4b6a8cf821 |
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
env
|
env
|
||||||
|
venv
|
||||||
__pycache__
|
__pycache__
|
||||||
.pytest_cache
|
.pytest_cache
|
||||||
.coverage
|
.coverage
|
||||||
|
|||||||
16
.vscode/settings.json
vendored
Normal file
16
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"cSpell.words": [
|
||||||
|
"Archiver",
|
||||||
|
"dbsuper",
|
||||||
|
"draggable",
|
||||||
|
"errored",
|
||||||
|
"excaliber",
|
||||||
|
"mangadex",
|
||||||
|
"readallcomics",
|
||||||
|
"Scrapable",
|
||||||
|
"skippable",
|
||||||
|
"Uncategorized",
|
||||||
|
"worktree",
|
||||||
|
"yoink"
|
||||||
|
]
|
||||||
|
}
|
||||||
2
setup.py
2
setup.py
@@ -28,5 +28,5 @@ setuptools.setup(
|
|||||||
'yoink = yoink.cli:yoink'
|
'yoink = yoink.cli:yoink'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
install_requires=['click', 'bs4', 'requests']
|
install_requires=['click', 'bs4', 'requests', 'click-default-group']
|
||||||
)
|
)
|
||||||
10
yoink/cli.py
10
yoink/cli.py
@@ -6,7 +6,7 @@ import click
|
|||||||
from click_default_group import DefaultGroup
|
from click_default_group import DefaultGroup
|
||||||
|
|
||||||
from yoink.config import YoinkConfig, app_root, config_from_file, library_path, config_path
|
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
|
return 1
|
||||||
|
|
||||||
click.echo(f'Downloading {comic.title}')
|
click.echo(f'Downloading {comic.title}')
|
||||||
comic.archiver.download()
|
download_comic_files(comic)
|
||||||
|
|
||||||
click.echo('Building comic archive')
|
click.echo('Building comic archive')
|
||||||
comic.archiver.generate_archive()
|
generate_archive(comic)
|
||||||
|
|
||||||
click.echo('Cleaning up')
|
click.echo('Cleaning up')
|
||||||
comic.archiver.cleanup_worktree()
|
clean_up(comic)
|
||||||
|
|
||||||
click.echo('Success')
|
click.echo('Success')
|
||||||
|
|
||||||
@@ -50,8 +50,6 @@ def init():
|
|||||||
|
|
||||||
|
|
||||||
@yoink.command()
|
@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.option('-s', '--series', is_flag=True, help='Download the entire series')
|
||||||
@click.argument('url')
|
@click.argument('url')
|
||||||
def download(url, series):
|
def download(url, series):
|
||||||
|
|||||||
104
yoink/comic.py
104
yoink/comic.py
@@ -1,5 +1,5 @@
|
|||||||
from urllib.error import HTTPError
|
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
|
from yoink.scraper import Scrapable
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -12,7 +12,6 @@ import re
|
|||||||
class Comic(Scrapable):
|
class Comic(Scrapable):
|
||||||
def __init__(self, url, path=None) -> None:
|
def __init__(self, url, path=None) -> None:
|
||||||
super().__init__(url)
|
super().__init__(url)
|
||||||
self.archiver = ComicArchiver(self, library=path)
|
|
||||||
|
|
||||||
def __is_supported_image(self, image):
|
def __is_supported_image(self, image):
|
||||||
return image.endswith('.jpg' or '.jpeg')
|
return image.endswith('.jpg' or '.jpeg')
|
||||||
@@ -83,7 +82,14 @@ class Comic(Scrapable):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def volume(self) -> int:
|
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
|
@property
|
||||||
def next(self) -> str:
|
def next(self) -> str:
|
||||||
@@ -106,70 +112,68 @@ class Comic(Scrapable):
|
|||||||
return not filename.endswith(config.skippable_images)
|
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:
|
def download_comic_files(comic: Comic, worktree = None):
|
||||||
self.queue.append(link)
|
if not worktree:
|
||||||
|
worktree = os.path.join(config.library_path, f'comics/{comic.title}')
|
||||||
def download(self) -> None:
|
|
||||||
|
|
||||||
if not os.path.exists(self.worktree):
|
if not os.path.exists(worktree):
|
||||||
os.makedirs(self.worktree, mode=0o777)
|
os.makedirs(worktree, mode=0o777)
|
||||||
|
|
||||||
opener = urllib.request.build_opener()
|
opener = urllib.request.build_opener()
|
||||||
opener.addheaders = [('User-Agent', 'Mozilla/5.0')]
|
opener.addheaders = [('User-Agent', "Mozilla/5.0")]
|
||||||
urllib.request.install_opener(opener)
|
urllib.request.install_opener(opener)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for index,url in enumerate(self.comic.filelist):
|
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]
|
||||||
|
|
||||||
if not url.endswith('.jpg'):
|
if len(file_extension) > 3:
|
||||||
formatted_file = os.path.join(self.worktree, f'{self.comic.title} ' + ''.join([str(index).zfill(3), '.jpg']))
|
file_extension = '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]
|
|
||||||
|
|
||||||
if len(file_extension) > 3:
|
formatted_file = f'{comic.title} - {page_number}.{file_extension}'
|
||||||
file_extension = 'jpg'
|
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}')
|
||||||
|
|
||||||
formatted_file = f'{self.comic.title} - {page_number}.{file_extension}'
|
def generate_archive(comic: Comic, worktree = None, archive_format = '.cbr'):
|
||||||
print(formatted_file, end='\r',)
|
if not worktree:
|
||||||
urllib.request.urlretrieve(url, filename=os.path.join(self.worktree, formatted_file))
|
worktree = os.path.join(config.library_path, f'comics/{comic.title}')
|
||||||
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}')
|
|
||||||
|
|
||||||
print()
|
archive_from = os.path.basename(worktree)
|
||||||
|
if os.path.exists(os.path.join(worktree, f'{comic.title}{archive_format}')):
|
||||||
|
return
|
||||||
|
|
||||||
def generate_archive(self, archive_format='.cbr'):
|
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}'))
|
||||||
|
|
||||||
archive_from = os.path.basename(self.worktree)
|
def clean_up(comic: Comic):
|
||||||
if os.path.exists(os.path.join(self.worktree, f'{self.comic.title}{archive_format}')):
|
worktree = os.path.join(config.library_path, f'comics/{comic.title}')
|
||||||
return
|
|
||||||
|
|
||||||
output = shutil.make_archive(self.comic.title, 'zip', self.worktree, self.worktree)
|
for image in os.listdir(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):
|
|
||||||
if not image.endswith(required_archive_files):
|
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__':
|
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-001-2021/') # no prev link
|
||||||
# comic = Comic('http://readallcomics.com/static-season-one-6-2022/') # no next link
|
# comic = Comic('http://readallcomics.com/static-season-one-6-2022/') # no next link
|
||||||
# comic = Comic('http://readallcomics.com/superman-vs-lobo-4-2022/')
|
# 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.next)
|
||||||
# print(comic.prev)
|
# print(comic.prev)
|
||||||
# print(comic.issue_number)
|
# print(comic.issue_number)
|
||||||
@@ -5,7 +5,7 @@ import unittest
|
|||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
|
|
||||||
from yoink.config import app_root, library_path, config_path, skippable_images, supported_sites, required_archive_files
|
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
|
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 = 'http://readallcomics.com/static-season-one-6-2022/'
|
||||||
self.test_comic_b = 'http://readallcomics.com/captain-marvel-vs-rogue-2021-part-1/'
|
self.test_comic_b = 'http://readallcomics.com/captain-marvel-vs-rogue-2021-part-1/'
|
||||||
self.comic = Comic(self.test_comic)
|
self.comic = Comic(self.test_comic)
|
||||||
self.archiver = ComicArchiver(self.comic)
|
|
||||||
self.remove_queue = []
|
self.remove_queue = []
|
||||||
self.expected_title = 'Static Season One 6 (2022)'
|
self.expected_title = 'Static Season One 6 (2022)'
|
||||||
self.expected_title_b = 'Captain Marvel vs. Rogue (2021 – Part 1)'
|
self.expected_title_b = 'Captain Marvel vs. Rogue (2021 – Part 1)'
|
||||||
self.expected_category = 'Static: Season One'
|
self.expected_category = 'Static: Season One'
|
||||||
self.expected_category_b = 'Captain Marvel vs. Rogue'
|
self.expected_category_b = 'Captain Marvel vs. Rogue'
|
||||||
self.expected_issue_num = 6
|
self.expected_issue_num = 6
|
||||||
|
self.expected_vol_num = 1
|
||||||
self.expected_next_url = None
|
self.expected_next_url = None
|
||||||
self.expected_prev_url = 'http://readallcomics.com/static-season-one-5-2022/'
|
self.expected_prev_url = 'http://readallcomics.com/static-season-one-5-2022/'
|
||||||
self.erroneous_comic = 'http://readallcomics.com/static-season-one-4-2021/'
|
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):
|
def test_001_comic_has_valid_title(self):
|
||||||
self.assertEqual(self.expected_title, self.comic.title)
|
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)
|
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)
|
self.assertEqual(len(os.listdir(os.path.join(library_path, 'comics'))), 0)
|
||||||
|
|
||||||
def test_004_comic_folder_created_and_populated(self):
|
def test_005_comic_folder_created_and_populated(self):
|
||||||
self.archiver.download()
|
download_comic_files(self.comic)
|
||||||
self.assertTrue(os.path.exists(os.path.join(library_path, f'comics/{self.comic.title}')))
|
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)
|
self.assertGreater(len(os.listdir(os.path.join(library_path, f'comics/{self.comic.title}'))), 0)
|
||||||
|
|
||||||
def test_005_comic_archive_generated(self):
|
def test_006_comic_archive_generated(self):
|
||||||
self.archiver.generate_archive()
|
generate_archive(self.comic)
|
||||||
self.assertTrue(os.path.exists(os.path.join(library_path, f'comics/{self.comic.title}/{self.comic.title}.cbr')))
|
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):
|
def test_007_folder_cleaned_after_archive_generation(self):
|
||||||
self.archiver.cleanup_worktree()
|
clean_up(self.comic)
|
||||||
self.assertLessEqual(len(os.listdir(os.path.join(library_path, f'comics/{self.comic.title}'))), 3)
|
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):
|
def test_008_comic_is_subclass_scrapable(self):
|
||||||
self.assertTrue(issubclass(Comic, Scrapable))
|
self.assertTrue(issubclass(Comic, Scrapable))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user