* [iwara] Add initial support
* [iwara] Add search support
* [iwara] Code cleanup
* [iwara] Small fixes and additions
* [iwara] Add tag support
* [iwara] Add mime-type to metadata
* [iwara] Refactor patterns/matching using urllib
* [iwara] Add unit tests
* [iwara] Update docs
* [iwara] Fix linting on older Python versions
* [iwara] update 'IwaraAPI' interface class
- define endpoints inside methods
- implement and use _call() and _pagination()
- cache auth tokens
* [iwara] split and rename 'profile' extractor
TODO:
- update test results
- simplify code
* [iwara] simplify '_user_params()' usage
* [iwara] update 'video' extractor
and move user data extraction into 'yield_video'
* [iwara] update 'image' extractor
and move user info extraction into 'yield_image()'
* [iwara] update 'playlist' extractor
* [iwara] update 'search' extractor
* [iwara] update 'tag' extractor
* [iwara] simplify 'yield_image' usage
perform API calls to get full 'files' list inside the function
* [iwara] add video "image" test
* [iwara] provide 'date' metadata
* [iwara] simplify 'source()'
remove urllib.parse usage
* [iwara] small optimizations
* get("key", {}) -> get("key") or {}
* split("…", 1) -> partition("…")
* use f-strings for all patterns
* [iwara] add missing 'keyarg=1' to profile() memcache decorator
* [tests/iwara] update results
* [iwara] extract more 'user' metadata
* [iwara] update default format strings
include 'date' in filenames to order them chronologically
* [iwara] restructure image/video handling
- use less generators
- make processing individual media items non-fatal
* [iwara] fix login and token handling
* [iwara] add 'favorite' extractor
* [iwara] add 'following' and 'followers' extractors
---------
Co-authored-by: Mike Fährmann <mike_faehrmann@web.de>
This commit is contained in:
@@ -466,6 +466,7 @@ Description
|
||||
* ``idolcomplex``
|
||||
* ``imgbb``
|
||||
* ``inkbunny``
|
||||
* ``iwara``
|
||||
* ``kemono``
|
||||
* ``mangadex``
|
||||
* ``mangoxo``
|
||||
|
||||
@@ -413,6 +413,11 @@
|
||||
"sleep-request": "0.5-1.5",
|
||||
"videos": true
|
||||
},
|
||||
"iwara":
|
||||
{
|
||||
"username": "",
|
||||
"password": ""
|
||||
},
|
||||
"kemono":
|
||||
{
|
||||
"username": "",
|
||||
|
||||
@@ -517,6 +517,12 @@ Consider all listed sites to potentially be NSFW.
|
||||
<td>Games</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Iwara</td>
|
||||
<td>https://www.iwara.tv/</td>
|
||||
<td>Favorites, Followers, Followed Users, individual Images, Playlists, Search Results, Tag Searches, User Profiles, User Images, User Playlists, User Videos, Videos</td>
|
||||
<td>Supported</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Keenspot</td>
|
||||
<td>http://www.keenspot.com/</td>
|
||||
|
||||
@@ -92,6 +92,7 @@ modules = [
|
||||
"issuu",
|
||||
"itaku",
|
||||
"itchio",
|
||||
"iwara",
|
||||
"jschan",
|
||||
"kabeuchi",
|
||||
"keenspot",
|
||||
|
||||
440
gallery_dl/extractor/iwara.py
Normal file
440
gallery_dl/extractor/iwara.py
Normal file
@@ -0,0 +1,440 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License version 2 as
|
||||
# published by the Free Software Foundation.
|
||||
|
||||
"""Extractors for https://www.iwara.tv/"""
|
||||
|
||||
from .common import Extractor, Message, Dispatch
|
||||
from .. import text, util, exception
|
||||
from ..cache import cache, memcache
|
||||
import hashlib
|
||||
|
||||
BASE_PATTERN = r"(?:https?://)?(?:www\.)?iwara\.tv"
|
||||
USER_PATTERN = rf"{BASE_PATTERN}/profile/([^/?#]+)"
|
||||
|
||||
|
||||
class IwaraExtractor(Extractor):
|
||||
"""Base class for iwara.tv extractors"""
|
||||
category = "iwara"
|
||||
root = "https://www.iwara.tv"
|
||||
directory_fmt = ("{category}", "{user[name]}")
|
||||
filename_fmt = "{date} {id} {title[:200]} {filename}.{extension}"
|
||||
archive_fmt = "{type} {user[name]} {id} {file_id}"
|
||||
|
||||
def _init(self):
|
||||
self.api = IwaraAPI(self)
|
||||
|
||||
def items_image(self, images, user=None):
|
||||
for image in images:
|
||||
try:
|
||||
if "image" in image:
|
||||
# could extract 'date_favorited' here
|
||||
image = image["image"]
|
||||
if not (files := image.get("files")):
|
||||
image = self.api.image(image["id"])
|
||||
files = image["files"]
|
||||
|
||||
group_info = self.extract_media_info(image, "file", False)
|
||||
group_info["user"] = (self.extract_user_info(image)
|
||||
if user is None else user)
|
||||
except Exception as exc:
|
||||
self.status |= 1
|
||||
self.log.error("Failed to process image %s (%s: %s)",
|
||||
image["id"], exc.__class__.__name__, exc)
|
||||
continue
|
||||
|
||||
yield Message.Directory, group_info
|
||||
for file in files:
|
||||
file_info = self.extract_media_info(file, None)
|
||||
file_id = file_info["file_id"]
|
||||
url = (f"https://i.iwara.tv/image/original/"
|
||||
f"{file_id}/{file_id}.{file_info['extension']}")
|
||||
yield Message.Url, url, {**file_info, **group_info}
|
||||
|
||||
def items_video(self, videos, user=None):
|
||||
for video in videos:
|
||||
try:
|
||||
if "video" in video:
|
||||
video = video["video"]
|
||||
if "fileUrl" not in video:
|
||||
video = self.api.video(video["id"])
|
||||
file_url = video["fileUrl"]
|
||||
sources = self.api.source(file_url)
|
||||
source = next((s for s in sources
|
||||
if s.get("name") == "Source"), None)
|
||||
download_url = source.get('src', {}).get('download')
|
||||
|
||||
info = self.extract_media_info(video, "file")
|
||||
info["user"] = (self.extract_user_info(video)
|
||||
if user is None else user)
|
||||
except Exception as exc:
|
||||
self.status |= 1
|
||||
self.log.error("Failed to process video %s (%s: %s)",
|
||||
video["id"], exc.__class__.__name__, exc)
|
||||
continue
|
||||
|
||||
yield Message.Directory, info
|
||||
yield Message.Url, f"https:{download_url}", info
|
||||
|
||||
def items_user(self, users, key):
|
||||
base = f"{self.root}/profile/"
|
||||
for user in users:
|
||||
user = user[key]
|
||||
if (username := user["username"]) is None:
|
||||
continue
|
||||
user["_extractor"] = IwaraUserExtractor
|
||||
yield Message.Queue, f"{base}{username}", user
|
||||
|
||||
def extract_media_info(self, item, key, include_file_info=True):
|
||||
title = t.strip() if (t := item.get("title")) else ""
|
||||
|
||||
if include_file_info:
|
||||
file_info = item if key is None else item.get(key) or {}
|
||||
filename, _, extension = file_info.get("name", "").rpartition(".")
|
||||
|
||||
return {
|
||||
"id" : item["id"],
|
||||
"file_id" : file_info.get("id"),
|
||||
"title" : title,
|
||||
"filename" : filename,
|
||||
"extension": extension,
|
||||
"date" : text.parse_datetime(
|
||||
file_info.get("createdAt"), "%Y-%m-%dT%H:%M:%S.%fZ"),
|
||||
"date_updated": text.parse_datetime(
|
||||
file_info.get("updatedAt"), "%Y-%m-%dT%H:%M:%S.%fZ"),
|
||||
"mime" : file_info.get("mime"),
|
||||
"size" : file_info.get("size"),
|
||||
"width" : file_info.get("width"),
|
||||
"height" : file_info.get("height"),
|
||||
"duration" : file_info.get("duration"),
|
||||
"type" : file_info.get("type"),
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"id" : item["id"],
|
||||
"title": title,
|
||||
}
|
||||
|
||||
def extract_user_info(self, profile):
|
||||
user = profile.get("user") or {}
|
||||
return {
|
||||
"id" : user.get("id"),
|
||||
"name" : user.get("username"),
|
||||
"nick" : user.get("name").strip(),
|
||||
"status" : user.get("status"),
|
||||
"role" : user.get("role"),
|
||||
"premium": user.get("premium"),
|
||||
"date" : text.parse_datetime(
|
||||
user.get("createdAt"), "%Y-%m-%dT%H:%M:%S.000Z"),
|
||||
"description": profile.get("body"),
|
||||
}
|
||||
|
||||
def _user_params(self):
|
||||
user, qs = self.groups
|
||||
params = text.parse_query(qs)
|
||||
profile = self.api.profile(user)
|
||||
params["user"] = profile["user"]["id"]
|
||||
return self.extract_user_info(profile), params
|
||||
|
||||
|
||||
class IwaraUserExtractor(Dispatch, IwaraExtractor):
|
||||
"""Extractor for iwara.tv profile pages"""
|
||||
pattern = rf"{USER_PATTERN}/?$"
|
||||
example = "https://www.iwara.tv/profile/USERNAME"
|
||||
|
||||
def items(self):
|
||||
base = f"{self.root}/profile/{self.groups[0]}/"
|
||||
return self._dispatch_extractors((
|
||||
(IwaraUserImagesExtractor , f"{base}images"),
|
||||
(IwaraUserVideosExtractor , f"{base}videos"),
|
||||
(IwaraUserPlaylistsExtractor, f"{base}playlists"),
|
||||
), ("user-images", "user-videos"))
|
||||
|
||||
|
||||
class IwaraUserImagesExtractor(IwaraExtractor):
|
||||
subcategory = "user-images"
|
||||
pattern = rf"{USER_PATTERN}/images(?:\?([^#]+))?"
|
||||
example = "https://www.iwara.tv/profile/USERNAME/images"
|
||||
|
||||
def items(self):
|
||||
user, params = self._user_params()
|
||||
return self.items_image(self.api.images(params), user)
|
||||
|
||||
|
||||
class IwaraUserVideosExtractor(IwaraExtractor):
|
||||
subcategory = "user-videos"
|
||||
pattern = rf"{USER_PATTERN}/videos(?:\?([^#]+))?"
|
||||
example = "https://www.iwara.tv/profile/USERNAME/videos"
|
||||
|
||||
def items(self):
|
||||
user, params = self._user_params()
|
||||
return self.items_video(self.api.videos(params), user)
|
||||
|
||||
|
||||
class IwaraUserPlaylistsExtractor(IwaraExtractor):
|
||||
subcategory = "user-playlists"
|
||||
pattern = rf"{USER_PATTERN}/playlists(?:\?([^#]+))?"
|
||||
example = "https://www.iwara.tv/profile/USERNAME/playlists"
|
||||
|
||||
def items(self):
|
||||
base = f"{self.root}/playlist/"
|
||||
|
||||
for playlist in self.api.playlists(self._user_params()[1]):
|
||||
playlist["_extractor"] = IwaraPlaylistExtractor
|
||||
url = f"{base}{playlist['id']}"
|
||||
yield Message.Queue, url, playlist
|
||||
|
||||
|
||||
class IwaraFollowingExtractor(IwaraExtractor):
|
||||
subcategory = "following"
|
||||
pattern = rf"{USER_PATTERN}/following"
|
||||
example = "https://www.iwara.tv/profile/USERNAME/following"
|
||||
|
||||
def items(self):
|
||||
uid = self.api.profile(self.groups[0])["user"]["id"]
|
||||
return self.items_user(self.api.user_following(uid), "user")
|
||||
|
||||
|
||||
class IwaraFollowersExtractor(IwaraExtractor):
|
||||
subcategory = "followers"
|
||||
pattern = rf"{USER_PATTERN}/followers"
|
||||
example = "https://www.iwara.tv/profile/USERNAME/followers"
|
||||
|
||||
def items(self):
|
||||
uid = self.api.profile(self.groups[0])["user"]["id"]
|
||||
return self.items_user(self.api.user_followers(uid), "follower")
|
||||
|
||||
|
||||
class IwaraImageExtractor(IwaraExtractor):
|
||||
"""Extractor for individual iwara.tv image pages"""
|
||||
subcategory = "image"
|
||||
pattern = rf"{BASE_PATTERN}/image/([^/?#]+)"
|
||||
example = "https://www.iwara.tv/image/ID"
|
||||
|
||||
def items(self):
|
||||
return self.items_image((self.api.image(self.groups[0]),))
|
||||
|
||||
|
||||
class IwaraVideoExtractor(IwaraExtractor):
|
||||
"""Extractor for individual iwara.tv videos"""
|
||||
subcategory = "video"
|
||||
pattern = rf"{BASE_PATTERN}/video/([^/?#]+)"
|
||||
example = "https://www.iwara.tv/video/ID"
|
||||
|
||||
def items(self):
|
||||
return self.items_video((self.api.video(self.groups[0]),))
|
||||
|
||||
|
||||
class IwaraPlaylistExtractor(IwaraExtractor):
|
||||
"""Extractor for individual iwara.tv playlist pages"""
|
||||
subcategory = "playlist"
|
||||
pattern = rf"{BASE_PATTERN}/playlist/([^/?#]+)"
|
||||
example = "https://www.iwara.tv/playlist/ID"
|
||||
|
||||
def items(self):
|
||||
return self.items_video(self.api.playlist(self.groups[0]))
|
||||
|
||||
|
||||
class IwaraFavoriteExtractor(IwaraExtractor):
|
||||
subcategory = "favorite"
|
||||
pattern = rf"{BASE_PATTERN}/favorites(?:/(image|video)s)?"
|
||||
example = "https://www.iwara.tv/favorites/videos"
|
||||
|
||||
def items(self):
|
||||
type = self.groups[0] or "vidoo"
|
||||
|
||||
results = self.api.favorites(type)
|
||||
if type == "image":
|
||||
return self.items_image(results)
|
||||
else:
|
||||
return self.items_video(results)
|
||||
|
||||
|
||||
class IwaraSearchExtractor(IwaraExtractor):
|
||||
"""Extractor for iwara.tv search pages"""
|
||||
subcategory = "search"
|
||||
pattern = rf"{BASE_PATTERN}/search\?([^#]+)"
|
||||
example = "https://www.iwara.tv/search?query=QUERY&type=TYPE"
|
||||
|
||||
def items(self):
|
||||
params = text.parse_query(self.groups[0])
|
||||
self.kwdict["search_type"] = type = params.get("type", "video")
|
||||
self.kwdict["search_tags"] = query = params.get("query")
|
||||
|
||||
results = self.api.search(type, query)
|
||||
if type == "image":
|
||||
return self.items_image(results)
|
||||
if type == "video":
|
||||
return self.items_video(results)
|
||||
|
||||
raise exception.AbortExtraction("Unsupported search type '%s'", type)
|
||||
|
||||
|
||||
class IwaraTagExtractor(IwaraExtractor):
|
||||
"""Extractor for iwara.tv tag search"""
|
||||
subcategory = "tag"
|
||||
pattern = rf"{BASE_PATTERN}/(videos|images)(?:\?([^#]+))?"
|
||||
example = "https://www.iwara.tv/videos?tags=TAGS"
|
||||
|
||||
def items(self):
|
||||
type, qs = self.groups
|
||||
params = text.parse_query(qs)
|
||||
self.kwdict["search_type"] = type
|
||||
self.kwdict["search_tags"] = params.get("tags")
|
||||
|
||||
if type == "images":
|
||||
return self.items_image(self.api.images(params))
|
||||
else:
|
||||
return self.items_video(self.api.videos(params))
|
||||
|
||||
|
||||
class IwaraAPI():
|
||||
"""Interface for the Iwara API"""
|
||||
root = "https://api.iwara.tv"
|
||||
|
||||
def __init__(self, extractor):
|
||||
self.extractor = extractor
|
||||
self.headers = {
|
||||
"Referer" : f"{extractor.root}/",
|
||||
"Content-Type": "application/json",
|
||||
"Origin" : extractor.root,
|
||||
}
|
||||
|
||||
self.username, self.password = extractor._get_auth_info()
|
||||
if not self.username:
|
||||
self.authenticate = util.noop
|
||||
|
||||
def image(self, image_id):
|
||||
endpoint = f"/image/{image_id}"
|
||||
return self._call(endpoint)
|
||||
|
||||
def video(self, video_id):
|
||||
endpoint = f"/video/{video_id}"
|
||||
return self._call(endpoint)
|
||||
|
||||
def playlist(self, playlist_id):
|
||||
endpoint = f"/playlist/{playlist_id}"
|
||||
return self._pagination(endpoint)
|
||||
|
||||
def detail(self, media):
|
||||
endpoint = f"/{media['type']}/{media['id']}"
|
||||
return self._call(endpoint)
|
||||
|
||||
def images(self, params):
|
||||
endpoint = "/images"
|
||||
params.setdefault("rating", "all")
|
||||
return self._pagination(endpoint, params)
|
||||
|
||||
def videos(self, params):
|
||||
endpoint = "/videos"
|
||||
params.setdefault("rating", "all")
|
||||
return self._pagination(endpoint, params)
|
||||
|
||||
def playlists(self, params):
|
||||
endpoint = "/playlists"
|
||||
return self._pagination(endpoint, params)
|
||||
|
||||
def collection(self, type, params):
|
||||
endpoint = f"/{type}s"
|
||||
params.setdefault("rating", "all")
|
||||
return self._pagination(endpoint, params)
|
||||
|
||||
def favorites(self, type):
|
||||
endpoint = f"/favorites/{type}s"
|
||||
return self._pagination(endpoint)
|
||||
|
||||
def search(self, type, query):
|
||||
endpoint = "/search"
|
||||
params = {"type": type, "query": query}
|
||||
return self._pagination(endpoint, params)
|
||||
|
||||
@memcache(keyarg=1)
|
||||
def profile(self, username):
|
||||
endpoint = f"/profile/{username}"
|
||||
return self._call(endpoint)
|
||||
|
||||
def user_following(self, user_id):
|
||||
endpoint = f"/user/{user_id}/following"
|
||||
return self._pagination(endpoint)
|
||||
|
||||
def user_followers(self, user_id):
|
||||
endpoint = f"/user/{user_id}/followers"
|
||||
return self._pagination(endpoint)
|
||||
|
||||
def source(self, file_url):
|
||||
base, _, query = file_url.partition("?")
|
||||
if not (expires := text.extr(query, "expires=", "&")):
|
||||
return ()
|
||||
file_id = base.rpartition("/")[2]
|
||||
sha_postfix = "5nFp9kmbNnHdAFhaqMvt"
|
||||
sha_key = f"{file_id}_{expires}_{sha_postfix}"
|
||||
hash = hashlib.sha1(sha_key.encode()).hexdigest()
|
||||
headers = {"X-Version": hash, **self.headers}
|
||||
return self.extractor.request_json(file_url, headers=headers)
|
||||
|
||||
def authenticate(self):
|
||||
self.headers["Authorization"] = self._authenticate_impl(self.username)
|
||||
|
||||
@cache(maxage=3600, keyarg=1)
|
||||
def _authenticate_impl(self, username):
|
||||
refresh_token = _refresh_token_cache(username)
|
||||
if refresh_token is None:
|
||||
self.extractor.log.info("Logging in as %s", username)
|
||||
|
||||
url = f"{self.root}/user/login"
|
||||
json = {
|
||||
"email" : username,
|
||||
"password": self.password
|
||||
}
|
||||
data = self.extractor.request_json(
|
||||
url, method="POST", headers=self.headers, json=json,
|
||||
fatal=False)
|
||||
|
||||
if not (refresh_token := data.get("token")):
|
||||
self.extractor.log.debug(data)
|
||||
raise exception.AuthenticationError(data.get("message"))
|
||||
_refresh_token_cache.update(username, refresh_token)
|
||||
|
||||
self.extractor.log.info("Refreshing access token for %s", username)
|
||||
|
||||
url = f"{self.root}/user/token"
|
||||
headers = {"Authorization": f"Bearer {refresh_token}", **self.headers}
|
||||
data = self.extractor.request_json(
|
||||
url, method="POST", headers=headers, fatal=False)
|
||||
|
||||
if not (access_token := data.get("accessToken")):
|
||||
self.extractor.log.debug(data)
|
||||
raise exception.AuthenticationError(data.get("message"))
|
||||
return f"Bearer {access_token}"
|
||||
|
||||
def _call(self, endpoint, params=None, headers=None):
|
||||
if headers is None:
|
||||
headers = self.headers
|
||||
|
||||
url = self.root + endpoint
|
||||
self.authenticate()
|
||||
return self.extractor.request_json(url, params=params, headers=headers)
|
||||
|
||||
def _pagination(self, endpoint, params=None):
|
||||
if params is None:
|
||||
params = {}
|
||||
params["page"] = 0
|
||||
params["limit"] = 50
|
||||
|
||||
while True:
|
||||
data = self._call(endpoint, params)
|
||||
|
||||
if not (results := data.get("results")):
|
||||
break
|
||||
yield from results
|
||||
|
||||
if len(results) < params["limit"]:
|
||||
break
|
||||
params["page"] += 1
|
||||
|
||||
|
||||
@cache(maxage=28*86400, keyarg=0)
|
||||
def _refresh_token_cache(username):
|
||||
return None
|
||||
@@ -287,6 +287,11 @@ SUBCATEGORY_MAP = {
|
||||
"saved": "Saved Posts",
|
||||
"tagged": "Tagged Posts",
|
||||
},
|
||||
"iwara": {
|
||||
"user-images": "User Images",
|
||||
"user-videos": "User Videos",
|
||||
"user-playlists": "User Playlists",
|
||||
},
|
||||
"kemono": {
|
||||
"discord" : "Discord Servers",
|
||||
"discord-server": "",
|
||||
@@ -475,6 +480,7 @@ AUTH_MAP = {
|
||||
"imgbb" : "Supported",
|
||||
"inkbunny" : "Supported",
|
||||
"instagram" : _COOKIES,
|
||||
"iwara" : "Supported",
|
||||
"kemono" : "Supported",
|
||||
"mangadex" : "Supported",
|
||||
"mangoxo" : "Supported",
|
||||
|
||||
292
test/results/iwara.py
Normal file
292
test/results/iwara.py
Normal file
@@ -0,0 +1,292 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License version 2 as
|
||||
# published by the Free Software Foundation.
|
||||
|
||||
from gallery_dl.extractor import iwara
|
||||
|
||||
|
||||
__tests__ = (
|
||||
{
|
||||
"#url" : "https://www.iwara.tv/profile/user2426993",
|
||||
"#class" : iwara.IwaraUserExtractor,
|
||||
"#results" : (
|
||||
"https://www.iwara.tv/profile/user2426993/images",
|
||||
"https://www.iwara.tv/profile/user2426993/videos",
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
"#url" : "https://www.iwara.tv/profile/user2426993/images",
|
||||
"#class" : iwara.IwaraUserImagesExtractor,
|
||||
"#results" : (
|
||||
"https://i.iwara.tv/image/original/215ef6c5-47a9-4894-aaef-7bbc7ed2b5d0/215ef6c5-47a9-4894-aaef-7bbc7ed2b5d0.png",
|
||||
"https://i.iwara.tv/image/original/382ce6bc-0393-43dd-adb7-dfd514a72011/382ce6bc-0393-43dd-adb7-dfd514a72011.png",
|
||||
"https://i.iwara.tv/image/original/57fad542-d5c7-4671-b295-f7c4886db80e/57fad542-d5c7-4671-b295-f7c4886db80e.png",
|
||||
"https://i.iwara.tv/image/original/80b61308-08b5-469b-ab86-b2d1a9819a32/80b61308-08b5-469b-ab86-b2d1a9819a32.png",
|
||||
),
|
||||
|
||||
"extension": "png",
|
||||
"type" : "image",
|
||||
},
|
||||
|
||||
{
|
||||
"#url" : "https://www.iwara.tv/profile/user2426993/videos",
|
||||
"#class" : iwara.IwaraUserVideosExtractor,
|
||||
"#pattern" : (
|
||||
r"https://\w+.iwara.tv/download\?filename=8035c1cb-6ac6-45df-a171-4d981a8339c5_Source.mp4&path=2025%2F07%2F04&expires=\d+.+",
|
||||
r"https://\w+.iwara.tv/download\?filename=59691a5b-dd5d-4476-919d-dc0d8c9ee11f_Source.mp4&path=2025%2F06%2F21&expires=\d+.+",
|
||||
),
|
||||
|
||||
"extension": "mp4",
|
||||
"type" : "video",
|
||||
},
|
||||
|
||||
{
|
||||
"#url" : "https://www.iwara.tv/profile/tyron82/playlists",
|
||||
"#class" : iwara.IwaraUserPlaylistsExtractor,
|
||||
"#pattern" : iwara.IwaraPlaylistExtractor.pattern,
|
||||
"#count" : range(10, 20),
|
||||
},
|
||||
|
||||
{
|
||||
"#url" : "https://www.iwara.tv/profile/tyron82/following",
|
||||
"#class" : iwara.IwaraFollowingExtractor,
|
||||
"#pattern" : iwara.IwaraUserExtractor.pattern,
|
||||
"#range" : "1-100",
|
||||
"#count" : 100,
|
||||
},
|
||||
|
||||
{
|
||||
"#url" : "https://www.iwara.tv/profile/tyron82/followers",
|
||||
"#class" : iwara.IwaraFollowersExtractor,
|
||||
"#pattern" : iwara.IwaraUserExtractor.pattern,
|
||||
"#range" : "1-100",
|
||||
"#count" : 100,
|
||||
},
|
||||
|
||||
{
|
||||
"#url" : "https://www.iwara.tv/playlist/01ea603a-4e70-4a36-bc28-dc717eebc2d7",
|
||||
"#category" : ("", "iwara", "playlist"),
|
||||
"#class" : iwara.IwaraPlaylistExtractor,
|
||||
"#pattern" : r"https://\w+.iwara.tv/download\?filename=b7708020-f531-4eb4-bfd3-c62f3d17927e_Source.mp4&path=2024%2F05%2F12&.+",
|
||||
"#count" : 1,
|
||||
|
||||
"id" : "OaoVL8nqijDjhB",
|
||||
"title" : "MMD.RuanMei's body modification",
|
||||
"file_id" : "b7708020-f531-4eb4-bfd3-c62f3d17927e",
|
||||
"filename" : "b7708020-f531-4eb4-bfd3-c62f3d17927e",
|
||||
"extension" : "mp4",
|
||||
"mime" : "video/mp4",
|
||||
"size" : 225197782,
|
||||
"width" : None,
|
||||
"height" : None,
|
||||
"duration" : 654,
|
||||
"type" : "video",
|
||||
"user" : {
|
||||
"date" : "dt:2020-05-15 09:59:32",
|
||||
"description": str,
|
||||
"id" : "c9a08dd5-3cb5-4d7c-b9bb-9eb4c55eda14",
|
||||
"name" : "arisananades",
|
||||
"nick" : "Arisananades",
|
||||
"premium" : False,
|
||||
"role" : "user",
|
||||
"status" : "active",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
"#url" : "https://www.iwara.tv/favorites/videos",
|
||||
"#class" : iwara.IwaraFavoriteExtractor,
|
||||
"#auth" : True,
|
||||
},
|
||||
|
||||
{
|
||||
"#url" : "https://www.iwara.tv/favorites/images",
|
||||
"#class" : iwara.IwaraFavoriteExtractor,
|
||||
"#auth" : True,
|
||||
},
|
||||
|
||||
{
|
||||
"#url" : "https://www.iwara.tv/search?query=genshin%20tentacle&type=video",
|
||||
"#category" : ("", "iwara", "search"),
|
||||
"#class" : iwara.IwaraSearchExtractor,
|
||||
"#count" : 5,
|
||||
|
||||
"extension" : "mp4",
|
||||
"mime" : "video/mp4",
|
||||
"width" : None,
|
||||
"height" : None,
|
||||
"type" : "video",
|
||||
"user": {
|
||||
"date" : "dt:2022-01-12 17:08:38",
|
||||
"description": str,
|
||||
"id" : "3ec40862-bcb6-4c2e-9f3b-6da3a00cc2d9",
|
||||
"name" : "nizipaco-kyu",
|
||||
"nick" : "Nizipaco - Kyu",
|
||||
"premium" : False,
|
||||
"role" : "user",
|
||||
"status" : "active",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
"#url" : "https://www.iwara.tv/search?query=genshin%20layla%20sex&type=image",
|
||||
"#category" : ("", "iwara", "search"),
|
||||
"#class" : iwara.IwaraSearchExtractor,
|
||||
"#count" : 20,
|
||||
|
||||
"duration" : None,
|
||||
"type" : "image",
|
||||
},
|
||||
|
||||
{
|
||||
"#url" : "https://www.iwara.tv/videos?tags=aether%2Ccitlali",
|
||||
"#category" : ("", "iwara", "tag"),
|
||||
"#class" : iwara.IwaraTagExtractor,
|
||||
"#pattern" : (
|
||||
r"https://\w+.iwara.tv/download\?filename=d8e3735d-048c-4525-adcf-4265c8b45444_Source.mp4&path=2025%2F05%2F15&expires=\d+&.+",
|
||||
r"https://\w+.iwara.tv/download\?filename=cc1a1aba-10b9-4e0f-a20f-5b9b17b33db1_Source.mp4&path=2025%2F04%2F03&expires=\d+&.+",
|
||||
r"https://\w+.iwara.tv/download\?filename=94a8a1b9-7586-4771-accd-6f9cb4c6a5a1_Source.mp4&path=2025%2F03%2F21&expires=\d+&.+",
|
||||
),
|
||||
|
||||
"user": {
|
||||
"id" : "2b4391f3-c46f-43f9-b18f-8bdb8a9df74f",
|
||||
"name": "lenoria",
|
||||
"nick": "lenoria",
|
||||
},
|
||||
"extension" : "mp4",
|
||||
"mime" : "video/mp4",
|
||||
"width" : None,
|
||||
"height" : None,
|
||||
"type" : "video",
|
||||
"search_tags" : "aether,citlali",
|
||||
"search_type" : "videos",
|
||||
"duration" : range(90, 200),
|
||||
},
|
||||
|
||||
{
|
||||
"#url" : "https://www.iwara.tv/images?tags=genshin_impact%2Ccitlali",
|
||||
"#category" : ("", "iwara", "tag"),
|
||||
"#class" : iwara.IwaraTagExtractor,
|
||||
"#results" : (
|
||||
"https://i.iwara.tv/image/original/c442c69f-30fb-4fd4-8f8f-338bbc77c07d/c442c69f-30fb-4fd4-8f8f-338bbc77c07d.jpg",
|
||||
"https://i.iwara.tv/image/original/7b53cc07-3640-4749-8c11-6da5f5a292a0/7b53cc07-3640-4749-8c11-6da5f5a292a0.jpg",
|
||||
"https://i.iwara.tv/image/original/373cc1cb-028e-44bd-aef3-3400de4f995b/373cc1cb-028e-44bd-aef3-3400de4f995b.jpg",
|
||||
"https://i.iwara.tv/image/original/0256b01b-8b4d-47f7-894d-2aceba6b8ab8/0256b01b-8b4d-47f7-894d-2aceba6b8ab8.jpg",
|
||||
"https://i.iwara.tv/image/original/8541dab6-9c67-419d-8af8-2e040ae487dc/8541dab6-9c67-419d-8af8-2e040ae487dc.png",
|
||||
"https://i.iwara.tv/image/original/8eba51de-c618-4853-964f-25f526b58398/8eba51de-c618-4853-964f-25f526b58398.webm",
|
||||
),
|
||||
|
||||
"duration" : None,
|
||||
"extension" : {"jpg", "png", "webm"},
|
||||
"mime" : {"image/jpeg", "image/png", "video/webm"},
|
||||
"search_tags" : "genshin_impact,citlali",
|
||||
"search_type" : "images",
|
||||
"type" : "image",
|
||||
},
|
||||
|
||||
{
|
||||
"#url" : "https://www.iwara.tv/video/6QvQvzZnELJ9vv/bluearchive-rio",
|
||||
"#category" : ("", "iwara", "video"),
|
||||
"#class" : iwara.IwaraVideoExtractor,
|
||||
"#pattern" : r"https://\w+.iwara.tv/download\?filename=7ba6e734-b9df-4588-88fc-4eef2bbf5c56_Source.mp4&path=2025%2F07%2F05&expires=\d+&hash=[0-9a-f]{64}",
|
||||
"#count" : 1,
|
||||
|
||||
"user": {
|
||||
"id" : "b3f86af1-874c-41f1-b62e-4e4b736ad3a4",
|
||||
"name": "croove",
|
||||
"nick": "crooveNSFW",
|
||||
},
|
||||
"id" : "6QvQvzZnELJ9vv",
|
||||
"title" : "[BlueArchive / ブルアカ] Rio",
|
||||
"file_id" : "7ba6e734-b9df-4588-88fc-4eef2bbf5c56",
|
||||
"filename" : "7ba6e734-b9df-4588-88fc-4eef2bbf5c56",
|
||||
"extension" : "mp4",
|
||||
"mime" : "video/mp4",
|
||||
"size" : 86328642,
|
||||
"width" : None,
|
||||
"height" : None,
|
||||
"duration" : 107,
|
||||
"type" : "video",
|
||||
"date" : "dt:2025-07-05 06:49:56",
|
||||
"date_updated" : "dt:2025-07-05 06:50:14",
|
||||
},
|
||||
|
||||
{
|
||||
"#url" : "https://www.iwara.tv/image/5m3gLfcei6BQsL/sparkle",
|
||||
"#category" : ("", "iwara", "image"),
|
||||
"#class" : iwara.IwaraImageExtractor,
|
||||
"#pattern" : r"https://i.iwara.tv/image/original/[\w-]{36}/[\w-]{36}\.png",
|
||||
"#count" : 13,
|
||||
|
||||
"user": {
|
||||
"id" : "771d2b29-5935-43d7-85e1-30abbf47ccad",
|
||||
"name": "zcccz",
|
||||
"nick": "zcccz",
|
||||
},
|
||||
"id" : "5m3gLfcei6BQsL",
|
||||
"title" : "Sparkle",
|
||||
"extension" : "png",
|
||||
"mime" : "image/png",
|
||||
"type" : "image",
|
||||
"date" : "type:datetime",
|
||||
"date_updated" : "type:datetime",
|
||||
},
|
||||
|
||||
{
|
||||
"#url" : "https://www.iwara.tv/image/PbYJb57QqwrFp0",
|
||||
"#category" : ("", "iwara", "image"),
|
||||
"#class" : iwara.IwaraImageExtractor,
|
||||
"#results" : "https://i.iwara.tv/image/original/0302deee-9cd5-4c1f-b931-04caf329c0c7/0302deee-9cd5-4c1f-b931-04caf329c0c7.png",
|
||||
"#sha1_content" : "9fc2ae4d0d26d4b50c38ff2c5c235d33e8b56d1c",
|
||||
|
||||
"user": {
|
||||
"id" : "ef14099e-a6db-4325-9c67-51c0615985d5",
|
||||
"name": "sanka",
|
||||
"nick": "Cerodiers",
|
||||
},
|
||||
"id" : "PbYJb57QqwrFp0",
|
||||
"title" : "还没做完",
|
||||
"file_id" : "0302deee-9cd5-4c1f-b931-04caf329c0c7",
|
||||
"filename" : "0302deee-9cd5-4c1f-b931-04caf329c0c7",
|
||||
"extension" : "png",
|
||||
"mime" : "image/png",
|
||||
"size" : 3564514,
|
||||
"width" : 2560,
|
||||
"height" : 1440,
|
||||
"duration" : None,
|
||||
"type" : "image",
|
||||
"date" : "dt:2025-07-04 03:15:37",
|
||||
"date_updated" : "dt:2025-07-04 03:15:53",
|
||||
},
|
||||
|
||||
{
|
||||
"#url" : "https://www.iwara.tv/image/sjqkK5EobXucju/ellen-joe-dancing",
|
||||
"#comment" : "WebM video with sound classified as 'image'",
|
||||
"#class" : iwara.IwaraImageExtractor,
|
||||
"#results" : "https://i.iwara.tv/image/original/cf1686ac-9796-4213-bea3-71b6dcaac658/cf1686ac-9796-4213-bea3-71b6dcaac658.webm",
|
||||
|
||||
"date" : "dt:2025-07-07 17:06:47",
|
||||
"date_updated": "dt:2025-07-07 17:07:11",
|
||||
"duration" : None,
|
||||
"extension" : "webm",
|
||||
"file_id" : "cf1686ac-9796-4213-bea3-71b6dcaac658",
|
||||
"filename" : "cf1686ac-9796-4213-bea3-71b6dcaac658",
|
||||
"width" : 1366,
|
||||
"height" : 768,
|
||||
"id" : "sjqkK5EobXucju",
|
||||
"mime" : "video/webm",
|
||||
"size" : 4747505,
|
||||
"subcategory" : "image",
|
||||
"title" : "Ellen Joe Dancing To Body Shaming",
|
||||
"type" : "image",
|
||||
"user": {
|
||||
"id" : "f7625ea7-c1c8-416b-b929-a245892911a6",
|
||||
"name": "marzcade",
|
||||
"nick": "Marzcade",
|
||||
},
|
||||
},
|
||||
|
||||
)
|
||||
Reference in New Issue
Block a user