Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OneDrive Proxy #20

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions homepage/templates/homepage/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ <h2>While Mangadex is dead.</h2>
<div class="search-wrapper">
<div class="search-box">
<div class="cubari-logo"></div>
<input type="text" id="search" name="cubariInput" placeholder="Imgur, NH, MD, mangasee, git.io, raw GitHub link..."><button class="ico-btn icon-ri" id="search-button"></button>
<input type="text" id="search" name="cubariInput" placeholder="Imgur, NH, MD, mangasee, git.io, raw GitHub, OneDrive share link..."><button class="ico-btn icon-ri" id="search-button"></button>
</div>
<div id="status"><a href="https://old.reddit.com/r/manga/comments/s1x668/sl_cubarimoe_gist_links_are_now_deprecated">UPDATE: git.io deprecation</a><br><br>To unite several chapters under one gist link,<br>you can use this <a href="https://stirante.com/facaccimo/">useful tool</a> by Stirante.</div>
<div id="status"><a href="#" onclick="openModal('about')">UPDATE: OneDrive Shared Folders</a><br><br>To unite several chapters under one gist link,<br>you can use this <a href="https://stirante.com/facaccimo/">useful tool</a> by Stirante.</div>
</div>
</section>
</div>
Expand Down
4 changes: 3 additions & 1 deletion proxy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .sources.reddit import Reddit
from .sources.mangadventure import MangAdventure
from .sources.dynasty import Dynasty
from .sources.onedrive import OneDrive

sources = [
MangaDex(),
Expand All @@ -31,5 +32,6 @@
Imgbox(),
Reddit(),
Imgbb(),
Dynasty()
Dynasty(),
OneDrive(),
]
247 changes: 247 additions & 0 deletions proxy/sources/onedrive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
from datetime import datetime

from django.shortcuts import redirect
from django.urls import re_path

from ..source import ProxySource
from ..source.data import ChapterAPI, ProxyException, SeriesAPI, SeriesPage
from ..source.helpers import api_cache, encode, get_wrapper
from json import JSONDecodeError
from requests import HTTPError, RequestException

import re


class OneDrive(ProxySource):
"""
Receives a OneDrive share URL
Will parse subfolders up to one level:
"[Artist] Series Title/Ch. 01 - Chapter Title/images.ext" OR "Title/images.ext"
Expects chapter folders to be prefixed by the number, will abstact 'Ch.' and 'Chapter' prefixes
Series title will be the top-most folder name
If chapter number can't be guessed from folder title, assumes 1.
Meaning unidentified subfolders will result in a single chapter with all images
Chapter will be blank if it can't be parsed from sub-folder name
Cover will be the first `cover.ext` found in the tree or page 1 of chapter 1
Doesn't support volumes, always "Uncategorized"
"""

def get_reader_prefix(self):
return "onedrive"

def shortcut_instantiator(self):
def handler(request, series_id):
print(request, series_id)
return redirect(f"reader-{self.get_reader_prefix()}-chapter-page", series_id)

return [
re_path(r"(?:1drv)/(?P<series_id>[\d\w]+)/$", handler),
]

@staticmethod
def date_parser(timestamp: float):
timestamp = int(timestamp)
try:
date = datetime.utcfromtimestamp(timestamp)
except ValueError:
date = datetime.utcfromtimestamp(timestamp // 1000)
return [
date.year,
date.month - 1,
date.day,
date.hour,
date.minute,
date.second,
]

@api_cache(prefix="od_common_dt", time=300)
def od_common(self, meta_id):
def od_api(share_id: str) -> dict:
map = {"folders": [], "files": []}
od_series_api = (
f"https://api.onedrive.com/v1.0/shares/{share_id}/driveItem?$expand=children"
)
resp = get_wrapper(od_series_api)
print(f"Response code: {resp.status_code} {resp.url}")

if not resp.ok:
resp = get_wrapper(od_series_api, use_proxy=True)
print(f"Response code proxy: {resp.status_code} {resp.url}")

try:
resp.raise_for_status()
resp = resp.json()
except (HTTPError, JSONDecodeError, RequestException) as error:
raise ProxyException(f"Could not parse OneDrive folder `{share_id}`: {error}")

map["title"] = resp["name"]
try:
map["date"] = datetime.fromisoformat(
f"{resp.get('lastModifiedDateTime', '')[:19]}+00:00"
).timestamp()
except ValueError:
map["date"] = datetime.utcnow().timestamp()

for contents in resp["children"]:
if "file" in contents and (
"image" in contents or "image" in contents.get("file", {}).get("mimeType", "")
):
if contents["name"].startswith("cover."):
map["cover"] = contents["@content.downloadUrl"]
continue
map["files"].append(contents["@content.downloadUrl"])
elif "file" in contents and contents["name"].endswith(".json"):
try:
map["metadata"] = get_wrapper(contents["@content.downloadUrl"]).json()
except (JSONDecodeError, RequestException):
continue
if "folder" in contents:
map["folders"].append(contents.get("webUrl").split("/")[-1])
if not map.get("cover") and map["files"]:
map["cover"] = map["files"][0]
return map

chapters_dict = {
"1": {
"title": "",
"last_updated": None,
"groups": {
"OneDrive": [],
},
}
}
series_dict = {
"title": "",
"description": "",
"artist": None,
"author": None,
"cover": None,
}
series = od_api(meta_id)
series_dict["title"] = series.get("metadata", {}).get(
"title", self.parse_title(series["title"])[1]
)
has_artist = re.search(r"^\[(.+?)\] ", series["title"], re.IGNORECASE)
series_dict["description"] = series.get("metadata", {}).get("description", "")
series_dict["artist"] = series.get("metadata", {}).get(
"artist", has_artist.group(1) if has_artist else "Unknown"
)
series_dict["author"] = series.get("metadata", {}).get("author", series_dict["artist"])
series_dict["alt_title"] = (
series_dict["title"].replace(has_artist.group(), "") if has_artist else ""
)
series_dict["cover"] = series.get("metadata", {}).get("cover", series.get("cover"))

if series.get("files"):
chapters_dict["1"] = {
"title": series["title"],
"last_updated": series["date"],
"groups": {"OneDrive": series["files"]},
}

for subfolder in series.get("folders", {}):
folder = od_api(subfolder)
if not folder["files"]:
continue
if not series_dict["cover"]:
series_dict["cover"] = folder["files"][0]
title = self.parse_title(folder["title"])
chapters_dict[title[0]] = {
"title": title[1],
"last_updated": folder["date"],
"groups": {"OneDrive": folder["files"]},
}
series_dict["chapters"] = chapters_dict

chapter_list = [
[
ch[0], # Chapter Number
ch[0], # Chapter Number
ch[1]["title"], # Chapter Title
ch[0].replace(".", "-"), # Chapter Slug
"OneDrive", # Group
self.date_parser(ch[1]["last_updated"]), # Date
"Uncatecorized", # Volume Number
]
for ch in sorted(
chapters_dict.items(),
key=lambda m: float(m[0]),
# reverse=True,
)
]
groups_dict = {str(key): "OneDrive" for key in chapters_dict}

return {
"slug": meta_id,
"title": series_dict["title"],
"alt_title": series_dict["alt_title"],
"description": series_dict["description"],
"artist": series_dict["artist"],
"author": series_dict["author"],
"cover": series_dict["cover"],
"chapters": chapters_dict,
"chapter_list": chapter_list,
"groups": groups_dict,
"timestamp": series["date"],
}

def parse_title(self, title: str) -> tuple:
search = re.search(r"^(?:Ch\.? ?|Chapter )?0?([\d\.,]{1,5})(?: - )?", title, re.IGNORECASE)
ch = search.group(1) if search else "1"
ch_title = title if not search else title.replace(search.group(), "")
return (ch, ch_title)

@api_cache(prefix="od_series_dt", time=300)
def series_api_handler(self, meta_id):
data = self.od_common(meta_id)
return (
SeriesAPI(
slug=meta_id,
title=data["title"],
description=data["description"],
author=data["artist"],
artist=data["artist"],
groups=data["groups"],
cover=data["cover"],
chapters=data["chapters"],
)
if data
else None
)

@api_cache(prefix="od_pages_dt", time=300)
def chapter_api_handler(self, meta_id):
data = self.od_common(meta_id)
return (
ChapterAPI(
pages=[
page
for c in [ch["groups"]["OneDrive"] for ch in data["chapters"].values()]
for page in c
],
series=data["slug"],
chapter="1",
)
if data
else None
)

@api_cache(prefix="od_series_page_dt", time=300)
def series_page_handler(self, meta_id):
data = self.od_common(meta_id)
return (
SeriesPage(
series=data["title"],
alt_titles=[data["alt_title"]],
alt_titles_str=data["alt_title"],
slug=data["slug"],
cover_vol_url=data["cover"],
metadata=[["Author", data["author"]], ["Artist", data["artist"]]],
synopsis=data["description"],
author=data["artist"],
chapter_list=data["chapter_list"],
original_url=f"https://1drv.ms/f/{meta_id}",
)
if data
else None
)
6 changes: 6 additions & 0 deletions static_global/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ let error = '';
if(!result || !result[2]) return message('Reader could not understand the given link.', 1)
result = '/read/nhentai/' + result[2];
break;
case (/1drv\.ms\/f\/s![A-Z0-9a-z]+/.test(text)):
result = /1drv\.ms\/f\/(s![A-Z0-9a-z]+)\b/.exec(text);
console.log(result)
if (!result || !result[1]) return message('Reader could not understand the given link.', 1)
result = '/read/onedrive/' + result[1];
break;
case (/mangadex\.org\/title/.test(text)):
result = /(\/?)([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})/.exec(text)
if(!result || !result[2]) return message('Reader could not understand the given link.', 1)
Expand Down
5 changes: 4 additions & 1 deletion templates/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ <h2>What's this thing?</h2>
<p>This reader was born while developing Guya.moe, the website to host Kaguya. After a while we added a lot of useful features, and now we branched it into a separate domain. We don't host anything, this is just a web image viewer that can parse other websites.</p>

<h2>What are the sites you can proxy?</h2>
<p>Currently it supports imgur, nhentai and custom appropriately-formatted JSON hosted on GitHub Gists. This is an advanced feature and will be explained in detail below.</p>
<p>Currently it supports imgur, OneDrive, nhentai and custom appropriately-formatted JSON hosted on GitHub Gists. This is an advanced feature and will be explained in detail below.</p>

<h2>Disclaimer regarding OneDrive shares</h2>
<p>OneDrive links provide user information (look for the "More Details" pane on the "Info" tab).<br>Keep that in mind if you intend to share Cubari links from that source.<br>We can't opt-out of receiving that information, so the best we can do is pinky-swear we don't even read it.</p>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a little fud about burying this in our "about" page wall of text when we're not the only ones that can read this information. The share URL should have a "source" button that reveals the original resource:

image

That original URL should in some way reveal the original source, which would also reveal the drive's owner.

I've seen gist owners not realizing that their GitHub accounts are publicly visible so I'm still a bit wary about this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried hijacking the function to add a new pop-up element exclusively for this disclaimer, but my frontend-fu wasn't strong enough. IIRC I managed to make it show up but broke the close button 😅.


<h2>Are you the next Mangadex?</h2>
<p>No, but we might become a web version Tachiyomi. This is not a guarantee, though.</p>
Expand Down