Merge branch 'res'
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright 2019-2025 Mike Fährmann
|
# Copyright 2019-2026 Mike Fährmann
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License version 2 as
|
||||||
@@ -83,7 +83,7 @@ class _500pxExtractor(Extractor):
|
|||||||
data = {
|
data = {
|
||||||
"operationName": opname,
|
"operationName": opname,
|
||||||
"variables" : util.json_dumps(variables),
|
"variables" : util.json_dumps(variables),
|
||||||
"query" : QUERIES[opname],
|
"query" : self.utils("graphql", opname),
|
||||||
}
|
}
|
||||||
return self.request_json(
|
return self.request_json(
|
||||||
url, method="POST", headers=headers, json=data)["data"]
|
url, method="POST", headers=headers, json=data)["data"]
|
||||||
@@ -212,512 +212,3 @@ class _500pxImageExtractor(_500pxExtractor):
|
|||||||
def photos(self):
|
def photos(self):
|
||||||
edges = ({"node": {"legacyId": self.photo_id}},)
|
edges = ({"node": {"legacyId": self.photo_id}},)
|
||||||
return self._extend(edges)
|
return self._extend(edges)
|
||||||
|
|
||||||
|
|
||||||
QUERIES = {
|
|
||||||
|
|
||||||
"OtherPhotosQuery": """\
|
|
||||||
query OtherPhotosQuery($username: String!, $pageSize: Int) {
|
|
||||||
user: userByUsername(username: $username) {
|
|
||||||
...OtherPhotosPaginationContainer_user_RlXb8
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment OtherPhotosPaginationContainer_user_RlXb8 on User {
|
|
||||||
photos(first: $pageSize, privacy: PROFILE, sort: ID_DESC) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
legacyId
|
|
||||||
canonicalPath
|
|
||||||
width
|
|
||||||
height
|
|
||||||
name
|
|
||||||
isLikedByMe
|
|
||||||
notSafeForWork
|
|
||||||
photographer: uploader {
|
|
||||||
id
|
|
||||||
legacyId
|
|
||||||
username
|
|
||||||
displayName
|
|
||||||
canonicalPath
|
|
||||||
followedByUsers {
|
|
||||||
isFollowedByMe
|
|
||||||
}
|
|
||||||
}
|
|
||||||
images(sizes: [33, 35]) {
|
|
||||||
size
|
|
||||||
url
|
|
||||||
jpegUrl
|
|
||||||
webpUrl
|
|
||||||
id
|
|
||||||
}
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
cursor
|
|
||||||
}
|
|
||||||
totalCount
|
|
||||||
pageInfo {
|
|
||||||
endCursor
|
|
||||||
hasNextPage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
|
|
||||||
"OtherPhotosPaginationContainerQuery": """\
|
|
||||||
query OtherPhotosPaginationContainerQuery($username: String!, $pageSize: Int, $cursor: String) {
|
|
||||||
userByUsername(username: $username) {
|
|
||||||
...OtherPhotosPaginationContainer_user_3e6UuE
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment OtherPhotosPaginationContainer_user_3e6UuE on User {
|
|
||||||
photos(first: $pageSize, after: $cursor, privacy: PROFILE, sort: ID_DESC) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
legacyId
|
|
||||||
canonicalPath
|
|
||||||
width
|
|
||||||
height
|
|
||||||
name
|
|
||||||
isLikedByMe
|
|
||||||
notSafeForWork
|
|
||||||
photographer: uploader {
|
|
||||||
id
|
|
||||||
legacyId
|
|
||||||
username
|
|
||||||
displayName
|
|
||||||
canonicalPath
|
|
||||||
followedByUsers {
|
|
||||||
isFollowedByMe
|
|
||||||
}
|
|
||||||
}
|
|
||||||
images(sizes: [33, 35]) {
|
|
||||||
size
|
|
||||||
url
|
|
||||||
jpegUrl
|
|
||||||
webpUrl
|
|
||||||
id
|
|
||||||
}
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
cursor
|
|
||||||
}
|
|
||||||
totalCount
|
|
||||||
pageInfo {
|
|
||||||
endCursor
|
|
||||||
hasNextPage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
|
|
||||||
"ProfileRendererQuery": """\
|
|
||||||
query ProfileRendererQuery($username: String!) {
|
|
||||||
profile: userByUsername(username: $username) {
|
|
||||||
id
|
|
||||||
legacyId
|
|
||||||
userType: type
|
|
||||||
username
|
|
||||||
firstName
|
|
||||||
displayName
|
|
||||||
registeredAt
|
|
||||||
canonicalPath
|
|
||||||
avatar {
|
|
||||||
...ProfileAvatar_avatar
|
|
||||||
id
|
|
||||||
}
|
|
||||||
userProfile {
|
|
||||||
firstname
|
|
||||||
lastname
|
|
||||||
state
|
|
||||||
country
|
|
||||||
city
|
|
||||||
about
|
|
||||||
id
|
|
||||||
}
|
|
||||||
socialMedia {
|
|
||||||
website
|
|
||||||
twitter
|
|
||||||
instagram
|
|
||||||
facebook
|
|
||||||
id
|
|
||||||
}
|
|
||||||
coverPhotoUrl
|
|
||||||
followedByUsers {
|
|
||||||
totalCount
|
|
||||||
isFollowedByMe
|
|
||||||
}
|
|
||||||
followingUsers {
|
|
||||||
totalCount
|
|
||||||
}
|
|
||||||
membership {
|
|
||||||
expiryDate
|
|
||||||
membershipTier: tier
|
|
||||||
photoUploadQuota
|
|
||||||
refreshPhotoUploadQuotaAt
|
|
||||||
paymentStatus
|
|
||||||
id
|
|
||||||
}
|
|
||||||
profileTabs {
|
|
||||||
tabs {
|
|
||||||
name
|
|
||||||
visible
|
|
||||||
}
|
|
||||||
}
|
|
||||||
...EditCover_cover
|
|
||||||
photoStats {
|
|
||||||
likeCount
|
|
||||||
viewCount
|
|
||||||
}
|
|
||||||
photos(privacy: PROFILE) {
|
|
||||||
totalCount
|
|
||||||
}
|
|
||||||
licensingPhotos(status: ACCEPTED) {
|
|
||||||
totalCount
|
|
||||||
}
|
|
||||||
portfolio {
|
|
||||||
id
|
|
||||||
status
|
|
||||||
userDisabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment EditCover_cover on User {
|
|
||||||
coverPhotoUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment ProfileAvatar_avatar on UserAvatar {
|
|
||||||
images(sizes: [MEDIUM, LARGE]) {
|
|
||||||
size
|
|
||||||
url
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
|
|
||||||
"GalleriesDetailQueryRendererQuery": """\
|
|
||||||
query GalleriesDetailQueryRendererQuery($galleryOwnerLegacyId: ID!, $ownerLegacyId: String, $slug: String, $token: String, $pageSize: Int, $gallerySize: Int) {
|
|
||||||
galleries(galleryOwnerLegacyId: $galleryOwnerLegacyId, first: $gallerySize) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
legacyId
|
|
||||||
description
|
|
||||||
name
|
|
||||||
privacy
|
|
||||||
canonicalPath
|
|
||||||
notSafeForWork
|
|
||||||
buttonName
|
|
||||||
externalUrl
|
|
||||||
cover {
|
|
||||||
images(sizes: [35, 33]) {
|
|
||||||
size
|
|
||||||
webpUrl
|
|
||||||
jpegUrl
|
|
||||||
id
|
|
||||||
}
|
|
||||||
id
|
|
||||||
}
|
|
||||||
photos {
|
|
||||||
totalCount
|
|
||||||
}
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gallery: galleryByOwnerIdAndSlugOrToken(ownerLegacyId: $ownerLegacyId, slug: $slug, token: $token) {
|
|
||||||
...GalleriesDetailPaginationContainer_gallery_RlXb8
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment GalleriesDetailPaginationContainer_gallery_RlXb8 on Gallery {
|
|
||||||
id
|
|
||||||
legacyId
|
|
||||||
name
|
|
||||||
privacy
|
|
||||||
notSafeForWork
|
|
||||||
ownPhotosOnly
|
|
||||||
canonicalPath
|
|
||||||
publicSlug
|
|
||||||
lastPublishedAt
|
|
||||||
photosAddedSinceLastPublished
|
|
||||||
reportStatus
|
|
||||||
creator {
|
|
||||||
legacyId
|
|
||||||
id
|
|
||||||
}
|
|
||||||
cover {
|
|
||||||
images(sizes: [33, 32, 36, 2048]) {
|
|
||||||
url
|
|
||||||
size
|
|
||||||
webpUrl
|
|
||||||
id
|
|
||||||
}
|
|
||||||
id
|
|
||||||
}
|
|
||||||
description
|
|
||||||
externalUrl
|
|
||||||
buttonName
|
|
||||||
photos(first: $pageSize) {
|
|
||||||
totalCount
|
|
||||||
edges {
|
|
||||||
cursor
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
legacyId
|
|
||||||
canonicalPath
|
|
||||||
name
|
|
||||||
description
|
|
||||||
category
|
|
||||||
uploadedAt
|
|
||||||
location
|
|
||||||
width
|
|
||||||
height
|
|
||||||
isLikedByMe
|
|
||||||
photographer: uploader {
|
|
||||||
id
|
|
||||||
legacyId
|
|
||||||
username
|
|
||||||
displayName
|
|
||||||
canonicalPath
|
|
||||||
avatar {
|
|
||||||
images(sizes: SMALL) {
|
|
||||||
url
|
|
||||||
id
|
|
||||||
}
|
|
||||||
id
|
|
||||||
}
|
|
||||||
followedByUsers {
|
|
||||||
totalCount
|
|
||||||
isFollowedByMe
|
|
||||||
}
|
|
||||||
}
|
|
||||||
images(sizes: [33, 32]) {
|
|
||||||
size
|
|
||||||
url
|
|
||||||
webpUrl
|
|
||||||
id
|
|
||||||
}
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pageInfo {
|
|
||||||
endCursor
|
|
||||||
hasNextPage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
|
|
||||||
"GalleriesDetailPaginationContainerQuery": """\
|
|
||||||
query GalleriesDetailPaginationContainerQuery($ownerLegacyId: String, $slug: String, $token: String, $pageSize: Int, $cursor: String) {
|
|
||||||
galleryByOwnerIdAndSlugOrToken(ownerLegacyId: $ownerLegacyId, slug: $slug, token: $token) {
|
|
||||||
...GalleriesDetailPaginationContainer_gallery_3e6UuE
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment GalleriesDetailPaginationContainer_gallery_3e6UuE on Gallery {
|
|
||||||
id
|
|
||||||
legacyId
|
|
||||||
name
|
|
||||||
privacy
|
|
||||||
notSafeForWork
|
|
||||||
ownPhotosOnly
|
|
||||||
canonicalPath
|
|
||||||
publicSlug
|
|
||||||
lastPublishedAt
|
|
||||||
photosAddedSinceLastPublished
|
|
||||||
reportStatus
|
|
||||||
creator {
|
|
||||||
legacyId
|
|
||||||
id
|
|
||||||
}
|
|
||||||
cover {
|
|
||||||
images(sizes: [33, 32, 36, 2048]) {
|
|
||||||
url
|
|
||||||
size
|
|
||||||
webpUrl
|
|
||||||
id
|
|
||||||
}
|
|
||||||
id
|
|
||||||
}
|
|
||||||
description
|
|
||||||
externalUrl
|
|
||||||
buttonName
|
|
||||||
photos(first: $pageSize, after: $cursor) {
|
|
||||||
totalCount
|
|
||||||
edges {
|
|
||||||
cursor
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
legacyId
|
|
||||||
canonicalPath
|
|
||||||
name
|
|
||||||
description
|
|
||||||
category
|
|
||||||
uploadedAt
|
|
||||||
location
|
|
||||||
width
|
|
||||||
height
|
|
||||||
isLikedByMe
|
|
||||||
photographer: uploader {
|
|
||||||
id
|
|
||||||
legacyId
|
|
||||||
username
|
|
||||||
displayName
|
|
||||||
canonicalPath
|
|
||||||
avatar {
|
|
||||||
images(sizes: SMALL) {
|
|
||||||
url
|
|
||||||
id
|
|
||||||
}
|
|
||||||
id
|
|
||||||
}
|
|
||||||
followedByUsers {
|
|
||||||
totalCount
|
|
||||||
isFollowedByMe
|
|
||||||
}
|
|
||||||
}
|
|
||||||
images(sizes: [33, 32]) {
|
|
||||||
size
|
|
||||||
url
|
|
||||||
webpUrl
|
|
||||||
id
|
|
||||||
}
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pageInfo {
|
|
||||||
endCursor
|
|
||||||
hasNextPage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
|
|
||||||
"LikedPhotosQueryRendererQuery": """\
|
|
||||||
query LikedPhotosQueryRendererQuery($pageSize: Int) {
|
|
||||||
...LikedPhotosPaginationContainer_query_RlXb8
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment LikedPhotosPaginationContainer_query_RlXb8 on Query {
|
|
||||||
likedPhotos(first: $pageSize) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
legacyId
|
|
||||||
canonicalPath
|
|
||||||
name
|
|
||||||
description
|
|
||||||
category
|
|
||||||
uploadedAt
|
|
||||||
location
|
|
||||||
width
|
|
||||||
height
|
|
||||||
isLikedByMe
|
|
||||||
notSafeForWork
|
|
||||||
tags
|
|
||||||
photographer: uploader {
|
|
||||||
id
|
|
||||||
legacyId
|
|
||||||
username
|
|
||||||
displayName
|
|
||||||
canonicalPath
|
|
||||||
avatar {
|
|
||||||
images {
|
|
||||||
url
|
|
||||||
id
|
|
||||||
}
|
|
||||||
id
|
|
||||||
}
|
|
||||||
followedByUsers {
|
|
||||||
totalCount
|
|
||||||
isFollowedByMe
|
|
||||||
}
|
|
||||||
}
|
|
||||||
images(sizes: [33, 35]) {
|
|
||||||
size
|
|
||||||
url
|
|
||||||
jpegUrl
|
|
||||||
webpUrl
|
|
||||||
id
|
|
||||||
}
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
cursor
|
|
||||||
}
|
|
||||||
pageInfo {
|
|
||||||
endCursor
|
|
||||||
hasNextPage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
|
|
||||||
"LikedPhotosPaginationContainerQuery": """\
|
|
||||||
query LikedPhotosPaginationContainerQuery($cursor: String, $pageSize: Int) {
|
|
||||||
...LikedPhotosPaginationContainer_query_3e6UuE
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment LikedPhotosPaginationContainer_query_3e6UuE on Query {
|
|
||||||
likedPhotos(first: $pageSize, after: $cursor) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
legacyId
|
|
||||||
canonicalPath
|
|
||||||
name
|
|
||||||
description
|
|
||||||
category
|
|
||||||
uploadedAt
|
|
||||||
location
|
|
||||||
width
|
|
||||||
height
|
|
||||||
isLikedByMe
|
|
||||||
notSafeForWork
|
|
||||||
tags
|
|
||||||
photographer: uploader {
|
|
||||||
id
|
|
||||||
legacyId
|
|
||||||
username
|
|
||||||
displayName
|
|
||||||
canonicalPath
|
|
||||||
avatar {
|
|
||||||
images {
|
|
||||||
url
|
|
||||||
id
|
|
||||||
}
|
|
||||||
id
|
|
||||||
}
|
|
||||||
followedByUsers {
|
|
||||||
totalCount
|
|
||||||
isFollowedByMe
|
|
||||||
}
|
|
||||||
}
|
|
||||||
images(sizes: [33, 35]) {
|
|
||||||
size
|
|
||||||
url
|
|
||||||
jpegUrl
|
|
||||||
webpUrl
|
|
||||||
id
|
|
||||||
}
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
cursor
|
|
||||||
}
|
|
||||||
pageInfo {
|
|
||||||
endCursor
|
|
||||||
hasNextPage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright 2018-2025 Mike Fährmann
|
# Copyright 2018-2026 Mike Fährmann
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License version 2 as
|
||||||
@@ -42,7 +42,7 @@ class BehanceExtractor(Extractor):
|
|||||||
"X-Requested-With": "XMLHttpRequest",
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
}
|
}
|
||||||
data = {
|
data = {
|
||||||
"query" : GRAPHQL_QUERIES[endpoint],
|
"query" : self.utils("graphql", endpoint),
|
||||||
"variables": variables,
|
"variables": variables,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,415 +284,3 @@ class BehanceCollectionExtractor(BehanceExtractor):
|
|||||||
if not items["pageInfo"]["hasNextPage"]:
|
if not items["pageInfo"]["hasNextPage"]:
|
||||||
return
|
return
|
||||||
variables["afterItem"] = items["pageInfo"]["endCursor"]
|
variables["afterItem"] = items["pageInfo"]["endCursor"]
|
||||||
|
|
||||||
|
|
||||||
GRAPHQL_QUERIES = {
|
|
||||||
"GetProfileProjects": """\
|
|
||||||
query GetProfileProjects($username: String, $after: String) {
|
|
||||||
user(username: $username) {
|
|
||||||
profileProjects(first: 12, after: $after) {
|
|
||||||
pageInfo {
|
|
||||||
endCursor
|
|
||||||
hasNextPage
|
|
||||||
}
|
|
||||||
nodes {
|
|
||||||
__typename
|
|
||||||
adminFlags {
|
|
||||||
mature_lock
|
|
||||||
privacy_lock
|
|
||||||
dmca_lock
|
|
||||||
flagged_lock
|
|
||||||
privacy_violation_lock
|
|
||||||
trademark_lock
|
|
||||||
spam_lock
|
|
||||||
eu_ip_lock
|
|
||||||
}
|
|
||||||
colors {
|
|
||||||
r
|
|
||||||
g
|
|
||||||
b
|
|
||||||
}
|
|
||||||
covers {
|
|
||||||
size_202 {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
size_404 {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
size_808 {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
features {
|
|
||||||
url
|
|
||||||
name
|
|
||||||
featuredOn
|
|
||||||
ribbon {
|
|
||||||
image
|
|
||||||
image2x
|
|
||||||
image3x
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fields {
|
|
||||||
id
|
|
||||||
label
|
|
||||||
slug
|
|
||||||
url
|
|
||||||
}
|
|
||||||
hasMatureContent
|
|
||||||
id
|
|
||||||
isFeatured
|
|
||||||
isHiddenFromWorkTab
|
|
||||||
isMatureReviewSubmitted
|
|
||||||
isOwner
|
|
||||||
isFounder
|
|
||||||
isPinnedToSubscriptionOverview
|
|
||||||
isPrivate
|
|
||||||
linkedAssets {
|
|
||||||
...sourceLinkFields
|
|
||||||
}
|
|
||||||
linkedAssetsCount
|
|
||||||
sourceFiles {
|
|
||||||
...sourceFileFields
|
|
||||||
}
|
|
||||||
matureAccess
|
|
||||||
modifiedOn
|
|
||||||
name
|
|
||||||
owners {
|
|
||||||
...OwnerFields
|
|
||||||
images {
|
|
||||||
size_50 {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
premium
|
|
||||||
publishedOn
|
|
||||||
stats {
|
|
||||||
appreciations {
|
|
||||||
all
|
|
||||||
}
|
|
||||||
views {
|
|
||||||
all
|
|
||||||
}
|
|
||||||
comments {
|
|
||||||
all
|
|
||||||
}
|
|
||||||
}
|
|
||||||
slug
|
|
||||||
tools {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
category
|
|
||||||
categoryLabel
|
|
||||||
categoryId
|
|
||||||
approved
|
|
||||||
url
|
|
||||||
backgroundColor
|
|
||||||
}
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment sourceFileFields on SourceFile {
|
|
||||||
__typename
|
|
||||||
sourceFileId
|
|
||||||
projectId
|
|
||||||
userId
|
|
||||||
title
|
|
||||||
assetId
|
|
||||||
renditionUrl
|
|
||||||
mimeType
|
|
||||||
size
|
|
||||||
category
|
|
||||||
licenseType
|
|
||||||
unitAmount
|
|
||||||
currency
|
|
||||||
tier
|
|
||||||
hidden
|
|
||||||
extension
|
|
||||||
hasUserPurchased
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment sourceLinkFields on LinkedAsset {
|
|
||||||
__typename
|
|
||||||
name
|
|
||||||
premium
|
|
||||||
url
|
|
||||||
category
|
|
||||||
licenseType
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment OwnerFields on User {
|
|
||||||
displayName
|
|
||||||
hasPremiumAccess
|
|
||||||
id
|
|
||||||
isFollowing
|
|
||||||
isProfileOwner
|
|
||||||
location
|
|
||||||
locationUrl
|
|
||||||
url
|
|
||||||
username
|
|
||||||
availabilityInfo {
|
|
||||||
availabilityTimeline
|
|
||||||
isAvailableFullTime
|
|
||||||
isAvailableFreelance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
|
|
||||||
"GetMoodboardItemsAndRecommendations": """\
|
|
||||||
query GetMoodboardItemsAndRecommendations(
|
|
||||||
$id: Int!
|
|
||||||
$firstItem: Int!
|
|
||||||
$afterItem: String
|
|
||||||
$shouldGetRecommendations: Boolean!
|
|
||||||
$shouldGetItems: Boolean!
|
|
||||||
$shouldGetMoodboardFields: Boolean!
|
|
||||||
) {
|
|
||||||
viewer @include(if: $shouldGetMoodboardFields) {
|
|
||||||
isOptedOutOfRecommendations
|
|
||||||
isAdmin
|
|
||||||
}
|
|
||||||
moodboard(id: $id) {
|
|
||||||
...moodboardFields @include(if: $shouldGetMoodboardFields)
|
|
||||||
|
|
||||||
items(first: $firstItem, after: $afterItem) @include(if: $shouldGetItems) {
|
|
||||||
pageInfo {
|
|
||||||
endCursor
|
|
||||||
hasNextPage
|
|
||||||
}
|
|
||||||
nodes {
|
|
||||||
...nodesFields
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
recommendedItems(first: 80) @include(if: $shouldGetRecommendations) {
|
|
||||||
nodes {
|
|
||||||
...nodesFields
|
|
||||||
fetchSource
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment moodboardFields on Moodboard {
|
|
||||||
id
|
|
||||||
label
|
|
||||||
privacy
|
|
||||||
followerCount
|
|
||||||
isFollowing
|
|
||||||
projectCount
|
|
||||||
url
|
|
||||||
isOwner
|
|
||||||
owners {
|
|
||||||
...OwnerFields
|
|
||||||
images {
|
|
||||||
size_50 {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
size_100 {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
size_115 {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
size_230 {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
size_138 {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
size_276 {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment projectFields on Project {
|
|
||||||
__typename
|
|
||||||
id
|
|
||||||
isOwner
|
|
||||||
publishedOn
|
|
||||||
matureAccess
|
|
||||||
hasMatureContent
|
|
||||||
modifiedOn
|
|
||||||
name
|
|
||||||
url
|
|
||||||
isPrivate
|
|
||||||
slug
|
|
||||||
license {
|
|
||||||
license
|
|
||||||
description
|
|
||||||
id
|
|
||||||
label
|
|
||||||
url
|
|
||||||
text
|
|
||||||
images
|
|
||||||
}
|
|
||||||
fields {
|
|
||||||
label
|
|
||||||
}
|
|
||||||
colors {
|
|
||||||
r
|
|
||||||
g
|
|
||||||
b
|
|
||||||
}
|
|
||||||
owners {
|
|
||||||
...OwnerFields
|
|
||||||
images {
|
|
||||||
size_50 {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
size_100 {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
size_115 {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
size_230 {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
size_138 {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
size_276 {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
covers {
|
|
||||||
size_original {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
size_max_808 {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
size_808 {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
size_404 {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
size_202 {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
size_230 {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
size_115 {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stats {
|
|
||||||
views {
|
|
||||||
all
|
|
||||||
}
|
|
||||||
appreciations {
|
|
||||||
all
|
|
||||||
}
|
|
||||||
comments {
|
|
||||||
all
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment exifDataValueFields on exifDataValue {
|
|
||||||
id
|
|
||||||
label
|
|
||||||
value
|
|
||||||
searchValue
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment nodesFields on MoodboardItem {
|
|
||||||
id
|
|
||||||
entityType
|
|
||||||
width
|
|
||||||
height
|
|
||||||
flexWidth
|
|
||||||
flexHeight
|
|
||||||
images {
|
|
||||||
size
|
|
||||||
url
|
|
||||||
}
|
|
||||||
|
|
||||||
entity {
|
|
||||||
... on Project {
|
|
||||||
...projectFields
|
|
||||||
}
|
|
||||||
|
|
||||||
... on ImageModule {
|
|
||||||
project {
|
|
||||||
...projectFields
|
|
||||||
}
|
|
||||||
|
|
||||||
colors {
|
|
||||||
r
|
|
||||||
g
|
|
||||||
b
|
|
||||||
}
|
|
||||||
|
|
||||||
exifData {
|
|
||||||
lens {
|
|
||||||
...exifDataValueFields
|
|
||||||
}
|
|
||||||
software {
|
|
||||||
...exifDataValueFields
|
|
||||||
}
|
|
||||||
makeAndModel {
|
|
||||||
...exifDataValueFields
|
|
||||||
}
|
|
||||||
focalLength {
|
|
||||||
...exifDataValueFields
|
|
||||||
}
|
|
||||||
iso {
|
|
||||||
...exifDataValueFields
|
|
||||||
}
|
|
||||||
location {
|
|
||||||
...exifDataValueFields
|
|
||||||
}
|
|
||||||
flash {
|
|
||||||
...exifDataValueFields
|
|
||||||
}
|
|
||||||
exposureMode {
|
|
||||||
...exifDataValueFields
|
|
||||||
}
|
|
||||||
shutterSpeed {
|
|
||||||
...exifDataValueFields
|
|
||||||
}
|
|
||||||
aperture {
|
|
||||||
...exifDataValueFields
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
... on MediaCollectionComponent {
|
|
||||||
project {
|
|
||||||
...projectFields
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment OwnerFields on User {
|
|
||||||
displayName
|
|
||||||
hasPremiumAccess
|
|
||||||
id
|
|
||||||
isFollowing
|
|
||||||
isProfileOwner
|
|
||||||
location
|
|
||||||
locationUrl
|
|
||||||
url
|
|
||||||
username
|
|
||||||
availabilityInfo {
|
|
||||||
availabilityTimeline
|
|
||||||
isAvailableFullTime
|
|
||||||
isAvailableFreelance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -354,6 +354,17 @@ class Extractor():
|
|||||||
seconds, reason)
|
seconds, reason)
|
||||||
time.sleep(seconds)
|
time.sleep(seconds)
|
||||||
|
|
||||||
|
def utils(self, module="", name=None):
|
||||||
|
module = (self.__class__.category if not module else
|
||||||
|
module[1:] if module[0] == "/" else
|
||||||
|
f"{self.__class__.category}_{module}")
|
||||||
|
if module in CACHE_UTILS:
|
||||||
|
res = CACHE_UTILS[module]
|
||||||
|
else:
|
||||||
|
res = CACHE_UTILS[module] = __import__(
|
||||||
|
"utils." + module, globals(), None, module, 1)
|
||||||
|
return res if name is None else getattr(res, name, None)
|
||||||
|
|
||||||
def input(self, prompt, echo=True):
|
def input(self, prompt, echo=True):
|
||||||
self._check_input_allowed(prompt)
|
self._check_input_allowed(prompt)
|
||||||
|
|
||||||
@@ -1128,6 +1139,7 @@ def _browser_useragent(browser):
|
|||||||
|
|
||||||
CACHE_ADAPTERS = {}
|
CACHE_ADAPTERS = {}
|
||||||
CACHE_COOKIES = {}
|
CACHE_COOKIES = {}
|
||||||
|
CACHE_UTILS = {}
|
||||||
CATEGORY_MAP = ()
|
CATEGORY_MAP = ()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -292,7 +292,8 @@ class DeviantartExtractor(Extractor):
|
|||||||
url = deviation["url"]
|
url = deviation["url"]
|
||||||
thumbs = deviation.get("thumbs") or deviation.get("files")
|
thumbs = deviation.get("thumbs") or deviation.get("files")
|
||||||
html = journal["html"]
|
html = journal["html"]
|
||||||
shadow = SHADOW_TEMPLATE.format_map(thumbs[0]) if thumbs else ""
|
tmpl = self.utils("journal")
|
||||||
|
shadow = tmpl.SHADOW.format_map(thumbs[0]) if thumbs else ""
|
||||||
|
|
||||||
if not html:
|
if not html:
|
||||||
self.log.warning("%s: Empty journal content", deviation["index"])
|
self.log.warning("%s: Empty journal content", deviation["index"])
|
||||||
@@ -308,14 +309,14 @@ class DeviantartExtractor(Extractor):
|
|||||||
|
|
||||||
if html.find('<div class="boxtop journaltop">', 0, 250) != -1:
|
if html.find('<div class="boxtop journaltop">', 0, 250) != -1:
|
||||||
needle = '<div class="boxtop journaltop">'
|
needle = '<div class="boxtop journaltop">'
|
||||||
header = HEADER_CUSTOM_TEMPLATE.format(
|
header = tmpl.HEADER_CUSTOM.format(
|
||||||
title=title, url=url, date=deviation["date"],
|
title=title, url=url, date=deviation["date"],
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
needle = '<div usr class="gr">'
|
needle = '<div usr class="gr">'
|
||||||
username = deviation["author"]["username"]
|
username = deviation["author"]["username"]
|
||||||
urlname = deviation.get("username") or username.lower()
|
urlname = deviation.get("username") or username.lower()
|
||||||
header = HEADER_TEMPLATE.format(
|
header = tmpl.HEADER.format(
|
||||||
title=title,
|
title=title,
|
||||||
url=url,
|
url=url,
|
||||||
userurl=f"{self.root}/{urlname}/",
|
userurl=f"{self.root}/{urlname}/",
|
||||||
@@ -326,9 +327,9 @@ class DeviantartExtractor(Extractor):
|
|||||||
if needle in html:
|
if needle in html:
|
||||||
html = html.replace(needle, header, 1)
|
html = html.replace(needle, header, 1)
|
||||||
else:
|
else:
|
||||||
html = JOURNAL_TEMPLATE_HTML_EXTRA.format(header, html)
|
html = tmpl.HTML_EXTRA.format(header, html)
|
||||||
|
|
||||||
html = JOURNAL_TEMPLATE_HTML.format(
|
html = tmpl.HTML.format(
|
||||||
title=title, html=html, shadow=shadow, css=css, cls=cls)
|
title=title, html=html, shadow=shadow, css=css, cls=cls)
|
||||||
|
|
||||||
deviation["extension"] = "htm"
|
deviation["extension"] = "htm"
|
||||||
@@ -345,7 +346,7 @@ class DeviantartExtractor(Extractor):
|
|||||||
text.unescape(text.remove_html(txt))
|
text.unescape(text.remove_html(txt))
|
||||||
for txt in (head or tail).split("<br />")
|
for txt in (head or tail).split("<br />")
|
||||||
)
|
)
|
||||||
txt = JOURNAL_TEMPLATE_TEXT.format(
|
txt = self.utils("journal").TEXT.format(
|
||||||
title=deviation["title"],
|
title=deviation["title"],
|
||||||
username=deviation["author"]["username"],
|
username=deviation["author"]["username"],
|
||||||
date=deviation["date"],
|
date=deviation["date"],
|
||||||
@@ -402,7 +403,7 @@ class DeviantartExtractor(Extractor):
|
|||||||
|
|
||||||
if html["type"] == "tiptap":
|
if html["type"] == "tiptap":
|
||||||
try:
|
try:
|
||||||
return self._tiptap_to_html(markup)
|
return self.utils("tiptap").to_html(markup)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.log.traceback(exc)
|
self.log.traceback(exc)
|
||||||
self.log.error("%s: '%s: %s'", deviation["index"],
|
self.log.error("%s: '%s: %s'", deviation["index"],
|
||||||
@@ -411,242 +412,6 @@ class DeviantartExtractor(Extractor):
|
|||||||
self.log.warning("%s: Unsupported '%s' markup.",
|
self.log.warning("%s: Unsupported '%s' markup.",
|
||||||
deviation["index"], html["type"])
|
deviation["index"], html["type"])
|
||||||
|
|
||||||
def _tiptap_to_html(self, markup):
|
|
||||||
html = []
|
|
||||||
|
|
||||||
html.append('<div data-editor-viewer="1" '
|
|
||||||
'class="_83r8m _2CKTq _3NjDa mDnFl">')
|
|
||||||
data = util.json_loads(markup)
|
|
||||||
for block in data["document"]["content"]:
|
|
||||||
self._tiptap_process_content(html, block)
|
|
||||||
html.append("</div>")
|
|
||||||
|
|
||||||
return "".join(html)
|
|
||||||
|
|
||||||
def _tiptap_process_content(self, html, content):
|
|
||||||
type = content["type"]
|
|
||||||
|
|
||||||
if type == "paragraph":
|
|
||||||
if children := content.get("content"):
|
|
||||||
html.append('<p style="')
|
|
||||||
|
|
||||||
if attrs := content.get("attrs"):
|
|
||||||
if align := attrs.get("textAlign"):
|
|
||||||
html.append("text-align:")
|
|
||||||
html.append(align)
|
|
||||||
html.append(";")
|
|
||||||
self._tiptap_process_indentation(html, attrs)
|
|
||||||
html.append('">')
|
|
||||||
else:
|
|
||||||
html.append('margin-inline-start:0px">')
|
|
||||||
|
|
||||||
for block in children:
|
|
||||||
self._tiptap_process_content(html, block)
|
|
||||||
html.append("</p>")
|
|
||||||
else:
|
|
||||||
html.append('<p class="empty-p"><br/></p>')
|
|
||||||
|
|
||||||
elif type == "text":
|
|
||||||
self._tiptap_process_text(html, content)
|
|
||||||
|
|
||||||
elif type == "heading":
|
|
||||||
attrs = content["attrs"]
|
|
||||||
level = str(attrs.get("level") or "3")
|
|
||||||
|
|
||||||
html.append("<h")
|
|
||||||
html.append(level)
|
|
||||||
html.append(' style="text-align:')
|
|
||||||
html.append(attrs.get("textAlign") or "left")
|
|
||||||
html.append('">')
|
|
||||||
html.append('<span style="')
|
|
||||||
self._tiptap_process_indentation(html, attrs)
|
|
||||||
html.append('">')
|
|
||||||
self._tiptap_process_children(html, content)
|
|
||||||
html.append("</span></h")
|
|
||||||
html.append(level)
|
|
||||||
html.append(">")
|
|
||||||
|
|
||||||
elif type in ("listItem", "bulletList", "orderedList", "blockquote"):
|
|
||||||
c = type[1]
|
|
||||||
tag = (
|
|
||||||
"li" if c == "i" else
|
|
||||||
"ul" if c == "u" else
|
|
||||||
"ol" if c == "r" else
|
|
||||||
"blockquote"
|
|
||||||
)
|
|
||||||
html.append("<" + tag + ">")
|
|
||||||
self._tiptap_process_children(html, content)
|
|
||||||
html.append("</" + tag + ">")
|
|
||||||
|
|
||||||
elif type == "anchor":
|
|
||||||
attrs = content["attrs"]
|
|
||||||
html.append('<a id="')
|
|
||||||
html.append(attrs.get("id") or "")
|
|
||||||
html.append('" data-testid="anchor"></a>')
|
|
||||||
|
|
||||||
elif type == "hardBreak":
|
|
||||||
html.append("<br/><br/>")
|
|
||||||
|
|
||||||
elif type == "horizontalRule":
|
|
||||||
html.append("<hr/>")
|
|
||||||
|
|
||||||
elif type == "da-deviation":
|
|
||||||
self._tiptap_process_deviation(html, content)
|
|
||||||
|
|
||||||
elif type == "da-mention":
|
|
||||||
user = content["attrs"]["user"]["username"]
|
|
||||||
html.append('<a href="https://www.deviantart.com/')
|
|
||||||
html.append(user.lower())
|
|
||||||
html.append('" data-da-type="da-mention" data-user="">@<!-- -->')
|
|
||||||
html.append(user)
|
|
||||||
html.append('</a>')
|
|
||||||
|
|
||||||
elif type == "da-gif":
|
|
||||||
attrs = content["attrs"]
|
|
||||||
width = str(attrs.get("width") or "")
|
|
||||||
height = str(attrs.get("height") or "")
|
|
||||||
url = text.escape(attrs.get("url") or "")
|
|
||||||
|
|
||||||
html.append('<div data-da-type="da-gif" data-width="')
|
|
||||||
html.append(width)
|
|
||||||
html.append('" data-height="')
|
|
||||||
html.append(height)
|
|
||||||
html.append('" data-alignment="')
|
|
||||||
html.append(attrs.get("alignment") or "")
|
|
||||||
html.append('" data-url="')
|
|
||||||
html.append(url)
|
|
||||||
html.append('" class="t61qu"><video role="img" autoPlay="" '
|
|
||||||
'muted="" loop="" style="pointer-events:none" '
|
|
||||||
'controlsList="nofullscreen" playsInline="" '
|
|
||||||
'aria-label="gif" data-da-type="da-gif" width="')
|
|
||||||
html.append(width)
|
|
||||||
html.append('" height="')
|
|
||||||
html.append(height)
|
|
||||||
html.append('" src="')
|
|
||||||
html.append(url)
|
|
||||||
html.append('" class="_1Fkk6"></video></div>')
|
|
||||||
|
|
||||||
elif type == "da-video":
|
|
||||||
src = text.escape(content["attrs"].get("src") or "")
|
|
||||||
html.append('<div data-testid="video" data-da-type="da-video" '
|
|
||||||
'data-src="')
|
|
||||||
html.append(src)
|
|
||||||
html.append('" class="_1Uxvs"><div data-canfs="yes" data-testid="v'
|
|
||||||
'ideo-inner" class="main-video" style="width:780px;hei'
|
|
||||||
'ght:438px"><div style="width:780px;height:438px">'
|
|
||||||
'<video src="')
|
|
||||||
html.append(src)
|
|
||||||
html.append('" style="width:100%;height:100%;" preload="auto" cont'
|
|
||||||
'rols=""></video></div></div></div>')
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.log.warning("Unsupported content type '%s'", type)
|
|
||||||
|
|
||||||
def _tiptap_process_text(self, html, content):
|
|
||||||
if marks := content.get("marks"):
|
|
||||||
close = []
|
|
||||||
for mark in marks:
|
|
||||||
type = mark["type"]
|
|
||||||
if type == "link":
|
|
||||||
attrs = mark.get("attrs") or {}
|
|
||||||
html.append('<a href="')
|
|
||||||
html.append(text.escape(attrs.get("href") or ""))
|
|
||||||
if "target" in attrs:
|
|
||||||
html.append('" target="')
|
|
||||||
html.append(attrs["target"])
|
|
||||||
html.append('" rel="')
|
|
||||||
html.append(attrs.get("rel") or
|
|
||||||
"noopener noreferrer nofollow ugc")
|
|
||||||
html.append('">')
|
|
||||||
close.append("</a>")
|
|
||||||
elif type == "bold":
|
|
||||||
html.append("<strong>")
|
|
||||||
close.append("</strong>")
|
|
||||||
elif type == "italic":
|
|
||||||
html.append("<em>")
|
|
||||||
close.append("</em>")
|
|
||||||
elif type == "underline":
|
|
||||||
html.append("<u>")
|
|
||||||
close.append("</u>")
|
|
||||||
elif type == "strike":
|
|
||||||
html.append("<s>")
|
|
||||||
close.append("</s>")
|
|
||||||
elif type == "textStyle" and len(mark) <= 1:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
self.log.warning("Unsupported text marker '%s'", type)
|
|
||||||
close.reverse()
|
|
||||||
html.append(text.escape(content["text"]))
|
|
||||||
html.extend(close)
|
|
||||||
else:
|
|
||||||
html.append(text.escape(content["text"]))
|
|
||||||
|
|
||||||
def _tiptap_process_children(self, html, content):
|
|
||||||
if children := content.get("content"):
|
|
||||||
for block in children:
|
|
||||||
self._tiptap_process_content(html, block)
|
|
||||||
|
|
||||||
def _tiptap_process_indentation(self, html, attrs):
|
|
||||||
itype = ("text-indent" if attrs.get("indentType") == "line" else
|
|
||||||
"margin-inline-start")
|
|
||||||
isize = str((attrs.get("indentation") or 0) * 24)
|
|
||||||
html.append(itype + ":" + isize + "px")
|
|
||||||
|
|
||||||
def _tiptap_process_deviation(self, html, content):
|
|
||||||
dev = content["attrs"]["deviation"]
|
|
||||||
media = dev.get("media") or ()
|
|
||||||
|
|
||||||
html.append('<div class="jjNX2">')
|
|
||||||
html.append('<figure class="Qf-HY" data-da-type="da-deviation" '
|
|
||||||
'data-deviation="" '
|
|
||||||
'data-width="" data-link="" data-alignment="center">')
|
|
||||||
|
|
||||||
if "baseUri" in media:
|
|
||||||
url, formats = self._eclipse_media(media)
|
|
||||||
full = formats["fullview"]
|
|
||||||
|
|
||||||
html.append('<a href="')
|
|
||||||
html.append(text.escape(dev["url"]))
|
|
||||||
html.append('" class="_3ouD5" style="margin:0 auto;display:flex;'
|
|
||||||
'align-items:center;justify-content:center;'
|
|
||||||
'overflow:hidden;width:780px;height:')
|
|
||||||
html.append(str(780 * full["h"] / full["w"]))
|
|
||||||
html.append('px">')
|
|
||||||
|
|
||||||
html.append('<img src="')
|
|
||||||
html.append(text.escape(url))
|
|
||||||
html.append('" alt="')
|
|
||||||
html.append(text.escape(dev["title"]))
|
|
||||||
html.append('" style="width:100%;max-width:100%;display:block"/>')
|
|
||||||
html.append("</a>")
|
|
||||||
|
|
||||||
elif "textContent" in dev:
|
|
||||||
html.append('<div class="_32Hs4" style="width:350px">')
|
|
||||||
|
|
||||||
html.append('<a href="')
|
|
||||||
html.append(text.escape(dev["url"]))
|
|
||||||
html.append('" class="_3ouD5">')
|
|
||||||
|
|
||||||
html.append('''\
|
|
||||||
<section class="Q91qI aG7Yi" style="width:350px;height:313px">\
|
|
||||||
<div class="_16ECM _1xMkk" aria-hidden="true">\
|
|
||||||
<svg height="100%" viewBox="0 0 15 12" preserveAspectRatio="xMidYMin slice" \
|
|
||||||
fill-rule="evenodd">\
|
|
||||||
<linearGradient x1="87.8481761%" y1="16.3690766%" \
|
|
||||||
x2="45.4107524%" y2="71.4898596%" id="app-root-3">\
|
|
||||||
<stop stop-color="#00FF62" offset="0%"></stop>\
|
|
||||||
<stop stop-color="#3197EF" stop-opacity="0" offset="100%"></stop>\
|
|
||||||
</linearGradient>\
|
|
||||||
<text class="_2uqbc" fill="url(#app-root-3)" text-anchor="end" x="15" y="11">J\
|
|
||||||
</text></svg></div><div class="_1xz9u">Literature</div><h3 class="_2WvKD">\
|
|
||||||
''')
|
|
||||||
html.append(text.escape(dev["title"]))
|
|
||||||
html.append('</h3><div class="_2CPLm">')
|
|
||||||
html.append(text.escape(dev["textContent"]["excerpt"]))
|
|
||||||
html.append('</div></section></a></div>')
|
|
||||||
|
|
||||||
html.append('</figure></div>')
|
|
||||||
|
|
||||||
def _extract_content(self, deviation):
|
def _extract_content(self, deviation):
|
||||||
content = deviation["content"]
|
content = deviation["content"]
|
||||||
|
|
||||||
@@ -827,25 +592,6 @@ x2="45.4107524%" y2="71.4898596%" id="app-root-3">\
|
|||||||
self.log.info("Unwatching %s", username)
|
self.log.info("Unwatching %s", username)
|
||||||
self.api.user_friends_unwatch(username)
|
self.api.user_friends_unwatch(username)
|
||||||
|
|
||||||
def _eclipse_media(self, media, format="preview"):
|
|
||||||
url = [media["baseUri"]]
|
|
||||||
|
|
||||||
formats = {
|
|
||||||
fmt["t"]: fmt
|
|
||||||
for fmt in media["types"]
|
|
||||||
}
|
|
||||||
|
|
||||||
if tokens := media.get("token") or ():
|
|
||||||
if len(tokens) <= 1:
|
|
||||||
fmt = formats[format]
|
|
||||||
if "c" in fmt:
|
|
||||||
url.append(fmt["c"].replace(
|
|
||||||
"<prettyName>", media["prettyName"]))
|
|
||||||
url.append("?token=")
|
|
||||||
url.append(tokens[-1])
|
|
||||||
|
|
||||||
return "".join(url), formats
|
|
||||||
|
|
||||||
def _eclipse_to_oauth(self, eclipse_api, deviations):
|
def _eclipse_to_oauth(self, eclipse_api, deviations):
|
||||||
for obj in deviations:
|
for obj in deviations:
|
||||||
deviation = obj["deviation"] if "deviation" in obj else obj
|
deviation = obj["deviation"] if "deviation" in obj else obj
|
||||||
@@ -1303,7 +1049,7 @@ class DeviantartDeviationExtractor(DeviantartExtractor):
|
|||||||
yield deviation
|
yield deviation
|
||||||
|
|
||||||
for index, post in enumerate(additional_media):
|
for index, post in enumerate(additional_media):
|
||||||
uri = self._eclipse_media(post["media"], "fullview")[0]
|
uri = eclipse_media(post["media"], "fullview")[0]
|
||||||
deviation["content"]["src"] = uri
|
deviation["content"]["src"] = uri
|
||||||
deviation["num"] += 1
|
deviation["num"] += 1
|
||||||
deviation["index_file"] = post["fileId"]
|
deviation["index_file"] = post["fileId"]
|
||||||
@@ -2124,6 +1870,9 @@ def _login_impl(extr, username, password):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||||
|
|
||||||
|
|
||||||
def id_from_base36(base36):
|
def id_from_base36(base36):
|
||||||
return util.bdecode(base36, _ALPHABET)
|
return util.bdecode(base36, _ALPHABET)
|
||||||
|
|
||||||
@@ -2132,116 +1881,21 @@ def base36_from_id(deviation_id):
|
|||||||
return util.bencode(int(deviation_id), _ALPHABET)
|
return util.bencode(int(deviation_id), _ALPHABET)
|
||||||
|
|
||||||
|
|
||||||
_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz"
|
def eclipse_media(media, format="preview"):
|
||||||
|
url = [media["baseUri"]]
|
||||||
|
|
||||||
|
formats = {
|
||||||
|
fmt["t"]: fmt
|
||||||
|
for fmt in media["types"]
|
||||||
|
}
|
||||||
|
|
||||||
###############################################################################
|
if tokens := media.get("token") or ():
|
||||||
# Journal Formats #############################################################
|
if len(tokens) <= 1:
|
||||||
|
fmt = formats[format]
|
||||||
|
if "c" in fmt:
|
||||||
|
url.append(fmt["c"].replace(
|
||||||
|
"<prettyName>", media["prettyName"]))
|
||||||
|
url.append("?token=")
|
||||||
|
url.append(tokens[-1])
|
||||||
|
|
||||||
SHADOW_TEMPLATE = """
|
return "".join(url), formats
|
||||||
<span class="shadow">
|
|
||||||
<img src="{src}" class="smshadow" width="{width}" height="{height}">
|
|
||||||
</span>
|
|
||||||
<br><br>
|
|
||||||
"""
|
|
||||||
|
|
||||||
HEADER_TEMPLATE = """<div usr class="gr">
|
|
||||||
<div class="metadata">
|
|
||||||
<h2><a href="{url}">{title}</a></h2>
|
|
||||||
<ul>
|
|
||||||
<li class="author">
|
|
||||||
by <span class="name"><span class="username-with-symbol u">
|
|
||||||
<a class="u regular username" href="{userurl}">{username}</a>\
|
|
||||||
<span class="user-symbol regular"></span></span></span>,
|
|
||||||
<span>{date}</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
|
|
||||||
HEADER_CUSTOM_TEMPLATE = """<div class='boxtop journaltop'>
|
|
||||||
<h2>
|
|
||||||
<img src="https://st.deviantart.net/minish/gruzecontrol/icons/journal.gif\
|
|
||||||
?2" style="vertical-align:middle" alt=""/>
|
|
||||||
<a href="{url}">{title}</a>
|
|
||||||
</h2>
|
|
||||||
Journal Entry: <span>{date}</span>
|
|
||||||
"""
|
|
||||||
|
|
||||||
JOURNAL_TEMPLATE_HTML = """text:<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>{title}</title>
|
|
||||||
<link rel="stylesheet" href="https://st.deviantart.net\
|
|
||||||
/css/deviantart-network_lc.css?3843780832"/>
|
|
||||||
<link rel="stylesheet" href="https://st.deviantart.net\
|
|
||||||
/css/group_secrets_lc.css?3250492874"/>
|
|
||||||
<link rel="stylesheet" href="https://st.deviantart.net\
|
|
||||||
/css/v6core_lc.css?4246581581"/>
|
|
||||||
<link rel="stylesheet" href="https://st.deviantart.net\
|
|
||||||
/css/sidebar_lc.css?1490570941"/>
|
|
||||||
<link rel="stylesheet" href="https://st.deviantart.net\
|
|
||||||
/css/writer_lc.css?3090682151"/>
|
|
||||||
<link rel="stylesheet" href="https://st.deviantart.net\
|
|
||||||
/css/v6loggedin_lc.css?3001430805"/>
|
|
||||||
<style>{css}</style>
|
|
||||||
<link rel="stylesheet" href="https://st.deviantart.net\
|
|
||||||
/roses/cssmin/core.css?1488405371919"/>
|
|
||||||
<link rel="stylesheet" href="https://st.deviantart.net\
|
|
||||||
/roses/cssmin/peeky.css?1487067424177"/>
|
|
||||||
<link rel="stylesheet" href="https://st.deviantart.net\
|
|
||||||
/roses/cssmin/desktop.css?1491362542749"/>
|
|
||||||
<link rel="stylesheet" href="https://static.parastorage.com/services\
|
|
||||||
/da-deviation/2bfd1ff7a9d6bf10d27b98dd8504c0399c3f9974a015785114b7dc6b\
|
|
||||||
/app.min.css"/>
|
|
||||||
</head>
|
|
||||||
<body id="deviantART-v7" class="bubble no-apps loggedout w960 deviantart">
|
|
||||||
<div id="output">
|
|
||||||
<div class="dev-page-container bubbleview">
|
|
||||||
<div class="dev-page-view view-mode-normal">
|
|
||||||
<div class="dev-view-main-content">
|
|
||||||
<div class="dev-view-deviation">
|
|
||||||
{shadow}
|
|
||||||
<div class="journal-wrapper tt-a">
|
|
||||||
<div class="journal-wrapper2">
|
|
||||||
<div class="journal {cls} journalcontrol">
|
|
||||||
{html}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
|
|
||||||
JOURNAL_TEMPLATE_HTML_EXTRA = """\
|
|
||||||
<div id="devskin0"><div class="negate-box-margin" style="">\
|
|
||||||
<div usr class="gr-box gr-genericbox"
|
|
||||||
><i usr class="gr1"><i></i></i
|
|
||||||
><i usr class="gr2"><i></i></i
|
|
||||||
><i usr class="gr3"><i></i></i
|
|
||||||
><div usr class="gr-top">
|
|
||||||
<i usr class="tri"></i>
|
|
||||||
{}
|
|
||||||
</div>
|
|
||||||
</div><div usr class="gr-body"><div usr class="gr">
|
|
||||||
<div class="grf-indent">
|
|
||||||
<div class="text">
|
|
||||||
{} </div>
|
|
||||||
</div>
|
|
||||||
</div></div>
|
|
||||||
<i usr class="gr3 gb"></i>
|
|
||||||
<i usr class="gr2 gb"></i>
|
|
||||||
<i usr class="gr1 gb gb1"></i> </div>
|
|
||||||
</div></div>"""
|
|
||||||
|
|
||||||
JOURNAL_TEMPLATE_TEXT = """text:{title}
|
|
||||||
by {username}, {date}
|
|
||||||
|
|
||||||
{content}
|
|
||||||
"""
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright 2016-2025 Mike Fährmann
|
# Copyright 2016-2026 Mike Fährmann
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License version 2 as
|
||||||
@@ -18,15 +18,15 @@ class LusciousExtractor(Extractor):
|
|||||||
cookies_domain = ".luscious.net"
|
cookies_domain = ".luscious.net"
|
||||||
root = "https://members.luscious.net"
|
root = "https://members.luscious.net"
|
||||||
|
|
||||||
def _graphql(self, op, variables, query):
|
def _request_graphql(self, opname, variables):
|
||||||
data = {
|
data = {
|
||||||
"id" : 1,
|
"id" : 1,
|
||||||
"operationName": op,
|
"operationName": opname,
|
||||||
"query" : query,
|
"query" : self.utils("graphql", opname),
|
||||||
"variables" : variables,
|
"variables" : variables,
|
||||||
}
|
}
|
||||||
response = self.request(
|
response = self.request(
|
||||||
f"{self.root}/graphql/nobatch/?operationName={op}",
|
f"{self.root}/graphql/nobatch/?operationName={opname}",
|
||||||
method="POST", json=data, fatal=False,
|
method="POST", json=data, fatal=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -49,11 +49,8 @@ class LusciousAlbumExtractor(LusciousExtractor):
|
|||||||
r"/(?:albums|pictures/c/[^/?#]+/album)/[^/?#]+_(\d+)")
|
r"/(?:albums|pictures/c/[^/?#]+/album)/[^/?#]+_(\d+)")
|
||||||
example = "https://luscious.net/albums/TITLE_12345/"
|
example = "https://luscious.net/albums/TITLE_12345/"
|
||||||
|
|
||||||
def __init__(self, match):
|
|
||||||
LusciousExtractor.__init__(self, match)
|
|
||||||
self.album_id = match[1]
|
|
||||||
|
|
||||||
def _init(self):
|
def _init(self):
|
||||||
|
self.album_id = self.groups[0]
|
||||||
self.gif = self.config("gif", False)
|
self.gif = self.config("gif", False)
|
||||||
|
|
||||||
def items(self):
|
def items(self):
|
||||||
@@ -83,98 +80,7 @@ class LusciousAlbumExtractor(LusciousExtractor):
|
|||||||
"id": self.album_id,
|
"id": self.album_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
query = """
|
album = self._request_graphql("AlbumGet", variables)["album"]["get"]
|
||||||
query AlbumGet($id: ID!) {
|
|
||||||
album {
|
|
||||||
get(id: $id) {
|
|
||||||
... on Album {
|
|
||||||
...AlbumStandard
|
|
||||||
}
|
|
||||||
... on MutationError {
|
|
||||||
errors {
|
|
||||||
code
|
|
||||||
message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment AlbumStandard on Album {
|
|
||||||
__typename
|
|
||||||
id
|
|
||||||
title
|
|
||||||
labels
|
|
||||||
description
|
|
||||||
created
|
|
||||||
modified
|
|
||||||
like_status
|
|
||||||
number_of_favorites
|
|
||||||
rating
|
|
||||||
status
|
|
||||||
marked_for_deletion
|
|
||||||
marked_for_processing
|
|
||||||
number_of_pictures
|
|
||||||
number_of_animated_pictures
|
|
||||||
slug
|
|
||||||
is_manga
|
|
||||||
url
|
|
||||||
download_url
|
|
||||||
permissions
|
|
||||||
cover {
|
|
||||||
width
|
|
||||||
height
|
|
||||||
size
|
|
||||||
url
|
|
||||||
}
|
|
||||||
created_by {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
display_name
|
|
||||||
user_title
|
|
||||||
avatar {
|
|
||||||
url
|
|
||||||
size
|
|
||||||
}
|
|
||||||
url
|
|
||||||
}
|
|
||||||
content {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
url
|
|
||||||
}
|
|
||||||
language {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
url
|
|
||||||
}
|
|
||||||
tags {
|
|
||||||
id
|
|
||||||
category
|
|
||||||
text
|
|
||||||
url
|
|
||||||
count
|
|
||||||
}
|
|
||||||
genres {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
slug
|
|
||||||
url
|
|
||||||
}
|
|
||||||
audiences {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
url
|
|
||||||
url
|
|
||||||
}
|
|
||||||
last_viewed_picture {
|
|
||||||
id
|
|
||||||
position
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
album = self._graphql("AlbumGet", variables, query)["album"]["get"]
|
|
||||||
if "errors" in album:
|
if "errors" in album:
|
||||||
raise exception.NotFoundError("album")
|
raise exception.NotFoundError("album")
|
||||||
|
|
||||||
@@ -204,66 +110,8 @@ fragment AlbumStandard on Album {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
query = """
|
|
||||||
query AlbumListOwnPictures($input: PictureListInput!) {
|
|
||||||
picture {
|
|
||||||
list(input: $input) {
|
|
||||||
info {
|
|
||||||
...FacetCollectionInfo
|
|
||||||
}
|
|
||||||
items {
|
|
||||||
...PictureStandardWithoutAlbum
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment FacetCollectionInfo on FacetCollectionInfo {
|
|
||||||
page
|
|
||||||
has_next_page
|
|
||||||
has_previous_page
|
|
||||||
total_items
|
|
||||||
total_pages
|
|
||||||
items_per_page
|
|
||||||
url_complete
|
|
||||||
url_filters_only
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment PictureStandardWithoutAlbum on Picture {
|
|
||||||
__typename
|
|
||||||
id
|
|
||||||
title
|
|
||||||
created
|
|
||||||
like_status
|
|
||||||
number_of_comments
|
|
||||||
number_of_favorites
|
|
||||||
status
|
|
||||||
width
|
|
||||||
height
|
|
||||||
resolution
|
|
||||||
aspect_ratio
|
|
||||||
url_to_original
|
|
||||||
url_to_video
|
|
||||||
is_animated
|
|
||||||
position
|
|
||||||
tags {
|
|
||||||
id
|
|
||||||
category
|
|
||||||
text
|
|
||||||
url
|
|
||||||
}
|
|
||||||
permissions
|
|
||||||
url
|
|
||||||
thumbnails {
|
|
||||||
width
|
|
||||||
height
|
|
||||||
size
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
while True:
|
while True:
|
||||||
data = self._graphql("AlbumListOwnPictures", variables, query)
|
data = self._request_graphql("AlbumListOwnPictures", variables)
|
||||||
yield from data["picture"]["list"]["items"]
|
yield from data["picture"]["list"]["items"]
|
||||||
|
|
||||||
if not data["picture"]["list"]["info"]["has_next_page"]:
|
if not data["picture"]["list"]["info"]["has_next_page"]:
|
||||||
@@ -278,12 +126,8 @@ class LusciousSearchExtractor(LusciousExtractor):
|
|||||||
r"/albums/list/?(?:\?([^#]+))?")
|
r"/albums/list/?(?:\?([^#]+))?")
|
||||||
example = "https://luscious.net/albums/list/?tagged=TAG"
|
example = "https://luscious.net/albums/list/?tagged=TAG"
|
||||||
|
|
||||||
def __init__(self, match):
|
|
||||||
LusciousExtractor.__init__(self, match)
|
|
||||||
self.query = match[1]
|
|
||||||
|
|
||||||
def items(self):
|
def items(self):
|
||||||
query = text.parse_query(self.query)
|
query = text.parse_query(self.groups[0])
|
||||||
display = query.pop("display", "date_newest")
|
display = query.pop("display", "date_newest")
|
||||||
page = query.pop("page", None)
|
page = query.pop("page", None)
|
||||||
|
|
||||||
@@ -295,89 +139,8 @@ class LusciousSearchExtractor(LusciousExtractor):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
query = """
|
|
||||||
query AlbumListWithPeek($input: AlbumListInput!) {
|
|
||||||
album {
|
|
||||||
list(input: $input) {
|
|
||||||
info {
|
|
||||||
...FacetCollectionInfo
|
|
||||||
}
|
|
||||||
items {
|
|
||||||
...AlbumMinimal
|
|
||||||
peek_thumbnails {
|
|
||||||
width
|
|
||||||
height
|
|
||||||
size
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment FacetCollectionInfo on FacetCollectionInfo {
|
|
||||||
page
|
|
||||||
has_next_page
|
|
||||||
has_previous_page
|
|
||||||
total_items
|
|
||||||
total_pages
|
|
||||||
items_per_page
|
|
||||||
url_complete
|
|
||||||
url_filters_only
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment AlbumMinimal on Album {
|
|
||||||
__typename
|
|
||||||
id
|
|
||||||
title
|
|
||||||
labels
|
|
||||||
description
|
|
||||||
created
|
|
||||||
modified
|
|
||||||
number_of_favorites
|
|
||||||
number_of_pictures
|
|
||||||
slug
|
|
||||||
is_manga
|
|
||||||
url
|
|
||||||
download_url
|
|
||||||
cover {
|
|
||||||
width
|
|
||||||
height
|
|
||||||
size
|
|
||||||
url
|
|
||||||
}
|
|
||||||
content {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
url
|
|
||||||
}
|
|
||||||
language {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
url
|
|
||||||
}
|
|
||||||
tags {
|
|
||||||
id
|
|
||||||
category
|
|
||||||
text
|
|
||||||
url
|
|
||||||
count
|
|
||||||
}
|
|
||||||
genres {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
slug
|
|
||||||
url
|
|
||||||
}
|
|
||||||
audiences {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
while True:
|
while True:
|
||||||
data = self._graphql("AlbumListWithPeek", variables, query)
|
data = self._request_graphql("AlbumListWithPeek", variables)
|
||||||
|
|
||||||
for album in data["album"]["list"]["items"]:
|
for album in data["album"]["list"]["items"]:
|
||||||
album["url"] = self.root + album["url"]
|
album["url"] = self.root + album["url"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright 2015-2025 Mike Fährmann
|
# Copyright 2015-2026 Mike Fährmann
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License version 2 as
|
||||||
@@ -60,7 +60,7 @@ class MangaparkBase():
|
|||||||
def _request_graphql(self, opname, variables):
|
def _request_graphql(self, opname, variables):
|
||||||
url = self.root + "/apo/"
|
url = self.root + "/apo/"
|
||||||
data = {
|
data = {
|
||||||
"query" : QUERIES[opname],
|
"query" : self.utils("graphql", opname),
|
||||||
"variables" : variables,
|
"variables" : variables,
|
||||||
"operationName": opname,
|
"operationName": opname,
|
||||||
}
|
}
|
||||||
@@ -177,183 +177,3 @@ class MangaparkMangaExtractor(MangaparkBase, Extractor):
|
|||||||
|
|
||||||
raise exception.AbortExtraction(
|
raise exception.AbortExtraction(
|
||||||
f"'{source}' does not match any available source")
|
f"'{source}' does not match any available source")
|
||||||
|
|
||||||
|
|
||||||
QUERIES = {
|
|
||||||
"Get_comicChapterList": """
|
|
||||||
query Get_comicChapterList($comicId: ID!) {
|
|
||||||
get_comicChapterList(comicId: $comicId) {
|
|
||||||
data {
|
|
||||||
id
|
|
||||||
dname
|
|
||||||
title
|
|
||||||
lang
|
|
||||||
urlPath
|
|
||||||
srcTitle
|
|
||||||
sourceId
|
|
||||||
dateCreate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
|
|
||||||
"Get_chapterNode": """
|
|
||||||
query Get_chapterNode($getChapterNodeId: ID!) {
|
|
||||||
get_chapterNode(id: $getChapterNodeId) {
|
|
||||||
data {
|
|
||||||
id
|
|
||||||
dname
|
|
||||||
lang
|
|
||||||
sourceId
|
|
||||||
srcTitle
|
|
||||||
dateCreate
|
|
||||||
comicNode{
|
|
||||||
id
|
|
||||||
}
|
|
||||||
imageFile {
|
|
||||||
urlList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
|
|
||||||
"Get_comicNode": """
|
|
||||||
query Get_comicNode($getComicNodeId: ID!) {
|
|
||||||
get_comicNode(id: $getComicNodeId) {
|
|
||||||
data {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
artists
|
|
||||||
authors
|
|
||||||
genres
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
|
|
||||||
"get_content_source_chapterList": """
|
|
||||||
query get_content_source_chapterList($sourceId: Int!) {
|
|
||||||
get_content_source_chapterList(
|
|
||||||
sourceId: $sourceId
|
|
||||||
) {
|
|
||||||
|
|
||||||
id
|
|
||||||
data {
|
|
||||||
|
|
||||||
|
|
||||||
id
|
|
||||||
sourceId
|
|
||||||
|
|
||||||
dbStatus
|
|
||||||
isNormal
|
|
||||||
isHidden
|
|
||||||
isDeleted
|
|
||||||
isFinal
|
|
||||||
|
|
||||||
dateCreate
|
|
||||||
datePublic
|
|
||||||
dateModify
|
|
||||||
lang
|
|
||||||
volume
|
|
||||||
serial
|
|
||||||
dname
|
|
||||||
title
|
|
||||||
urlPath
|
|
||||||
|
|
||||||
srcTitle srcColor
|
|
||||||
|
|
||||||
count_images
|
|
||||||
|
|
||||||
stat_count_post_child
|
|
||||||
stat_count_post_reply
|
|
||||||
stat_count_views_login
|
|
||||||
stat_count_views_guest
|
|
||||||
|
|
||||||
userId
|
|
||||||
userNode {
|
|
||||||
|
|
||||||
id
|
|
||||||
data {
|
|
||||||
|
|
||||||
id
|
|
||||||
name
|
|
||||||
uniq
|
|
||||||
avatarUrl
|
|
||||||
urlPath
|
|
||||||
|
|
||||||
verified
|
|
||||||
deleted
|
|
||||||
banned
|
|
||||||
|
|
||||||
dateCreate
|
|
||||||
dateOnline
|
|
||||||
|
|
||||||
stat_count_chapters_normal
|
|
||||||
stat_count_chapters_others
|
|
||||||
|
|
||||||
is_adm is_mod is_vip is_upr
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
disqusId
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
|
|
||||||
"get_content_comic_sources": """
|
|
||||||
query get_content_comic_sources($comicId: Int!, $dbStatuss: [String] = [], $userId: Int, $haveChapter: Boolean, $sortFor: String) {
|
|
||||||
get_content_comic_sources(
|
|
||||||
comicId: $comicId
|
|
||||||
dbStatuss: $dbStatuss
|
|
||||||
userId: $userId
|
|
||||||
haveChapter: $haveChapter
|
|
||||||
sortFor: $sortFor
|
|
||||||
) {
|
|
||||||
|
|
||||||
id
|
|
||||||
data{
|
|
||||||
|
|
||||||
id
|
|
||||||
|
|
||||||
dbStatus
|
|
||||||
isNormal
|
|
||||||
isHidden
|
|
||||||
isDeleted
|
|
||||||
|
|
||||||
lang name altNames authors artists
|
|
||||||
|
|
||||||
release
|
|
||||||
genres summary{code} extraInfo{code}
|
|
||||||
|
|
||||||
urlCover600
|
|
||||||
urlCover300
|
|
||||||
urlCoverOri
|
|
||||||
|
|
||||||
srcTitle srcColor
|
|
||||||
|
|
||||||
chapterCount
|
|
||||||
chapterNode_last {
|
|
||||||
id
|
|
||||||
data {
|
|
||||||
dateCreate datePublic dateModify
|
|
||||||
volume serial
|
|
||||||
dname title
|
|
||||||
urlPath
|
|
||||||
userNode {
|
|
||||||
id data {uniq name}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ class ScrolllerExtractor(Extractor):
|
|||||||
"Sec-Fetch-Site": "same-site",
|
"Sec-Fetch-Site": "same-site",
|
||||||
}
|
}
|
||||||
data = {
|
data = {
|
||||||
"query" : QUERIES[opname],
|
"query" : self.utils("graphql", opname),
|
||||||
"variables" : variables,
|
"variables" : variables,
|
||||||
"authorization": self.auth_token,
|
"authorization": self.auth_token,
|
||||||
}
|
}
|
||||||
@@ -206,144 +206,3 @@ class ScrolllerPostExtractor(ScrolllerExtractor):
|
|||||||
variables = {"url": "/" + self.groups[0]}
|
variables = {"url": "/" + self.groups[0]}
|
||||||
data = self._request_graphql("SubredditPostQuery", variables)
|
data = self._request_graphql("SubredditPostQuery", variables)
|
||||||
return (data["getPost"],)
|
return (data["getPost"],)
|
||||||
|
|
||||||
|
|
||||||
QUERIES = {
|
|
||||||
|
|
||||||
"SubredditPostQuery": """\
|
|
||||||
query SubredditPostQuery(
|
|
||||||
$url: String!
|
|
||||||
) {
|
|
||||||
getPost(
|
|
||||||
data: { url: $url }
|
|
||||||
) {
|
|
||||||
__typename id url title subredditId subredditTitle subredditUrl
|
|
||||||
redditPath isNsfw hasAudio fullLengthSource gfycatSource redgifsSource
|
|
||||||
ownerAvatar username displayName favoriteCount isPaid tags
|
|
||||||
commentsCount commentsRepliesCount isFavorite
|
|
||||||
albumContent { mediaSources { url width height isOptimized } }
|
|
||||||
mediaSources { url width height isOptimized }
|
|
||||||
blurredMediaSources { url width height isOptimized }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
|
|
||||||
"SubredditQuery": """\
|
|
||||||
query SubredditQuery(
|
|
||||||
$url: String!
|
|
||||||
$iterator: String
|
|
||||||
$sortBy: GallerySortBy
|
|
||||||
$filter: GalleryFilter
|
|
||||||
$limit: Int!
|
|
||||||
) {
|
|
||||||
getSubreddit(
|
|
||||||
data: {
|
|
||||||
url: $url,
|
|
||||||
iterator: $iterator,
|
|
||||||
filter: $filter,
|
|
||||||
limit: $limit,
|
|
||||||
sortBy: $sortBy
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
__typename id url title secondaryTitle description createdAt isNsfw
|
|
||||||
subscribers isComplete itemCount videoCount pictureCount albumCount
|
|
||||||
isPaid username tags isFollowing
|
|
||||||
banner { url width height isOptimized }
|
|
||||||
children {
|
|
||||||
iterator items {
|
|
||||||
__typename id url title subredditId subredditTitle subredditUrl
|
|
||||||
redditPath isNsfw hasAudio fullLengthSource gfycatSource
|
|
||||||
redgifsSource ownerAvatar username displayName favoriteCount
|
|
||||||
isPaid tags commentsCount commentsRepliesCount isFavorite
|
|
||||||
albumContent { mediaSources { url width height isOptimized } }
|
|
||||||
mediaSources { url width height isOptimized }
|
|
||||||
blurredMediaSources { url width height isOptimized }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
|
|
||||||
"SubredditChildrenQuery": """\
|
|
||||||
query SubredditChildrenQuery(
|
|
||||||
$subredditId: Int!
|
|
||||||
$iterator: String
|
|
||||||
$filter: GalleryFilter
|
|
||||||
$sortBy: GallerySortBy
|
|
||||||
$limit: Int!
|
|
||||||
$isNsfw: Boolean
|
|
||||||
) {
|
|
||||||
getSubredditChildren(
|
|
||||||
data: {
|
|
||||||
subredditId: $subredditId,
|
|
||||||
iterator: $iterator,
|
|
||||||
filter: $filter,
|
|
||||||
sortBy: $sortBy,
|
|
||||||
limit: $limit,
|
|
||||||
isNsfw: $isNsfw
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
iterator items {
|
|
||||||
__typename id url title subredditId subredditTitle subredditUrl
|
|
||||||
redditPath isNsfw hasAudio fullLengthSource gfycatSource
|
|
||||||
redgifsSource ownerAvatar username displayName favoriteCount isPaid
|
|
||||||
tags commentsCount commentsRepliesCount isFavorite
|
|
||||||
albumContent { mediaSources { url width height isOptimized } }
|
|
||||||
mediaSources { url width height isOptimized }
|
|
||||||
blurredMediaSources { url width height isOptimized }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
|
|
||||||
"GetFollowingSubreddits": """\
|
|
||||||
query GetFollowingSubreddits(
|
|
||||||
$iterator: String,
|
|
||||||
$limit: Int!,
|
|
||||||
$filter: GalleryFilter,
|
|
||||||
$isNsfw: Boolean,
|
|
||||||
$sortBy: GallerySortBy
|
|
||||||
) {
|
|
||||||
getFollowingSubreddits(
|
|
||||||
data: {
|
|
||||||
isNsfw: $isNsfw
|
|
||||||
limit: $limit
|
|
||||||
filter: $filter
|
|
||||||
iterator: $iterator
|
|
||||||
sortBy: $sortBy
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
iterator items {
|
|
||||||
__typename id url title secondaryTitle description createdAt isNsfw
|
|
||||||
subscribers isComplete itemCount videoCount pictureCount albumCount
|
|
||||||
isFollowing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
|
|
||||||
"LoginQuery": """\
|
|
||||||
query LoginQuery(
|
|
||||||
$username: String!,
|
|
||||||
$password: String!
|
|
||||||
) {
|
|
||||||
login(
|
|
||||||
username: $username,
|
|
||||||
password: $password
|
|
||||||
) {
|
|
||||||
username token expiresAt isAdmin status isPremium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
|
|
||||||
"ItemTypeQuery": """\
|
|
||||||
query ItemTypeQuery(
|
|
||||||
$url: String!
|
|
||||||
) {
|
|
||||||
getItemType(
|
|
||||||
url: $url
|
|
||||||
)
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright 2019-2025 Mike Fährmann
|
# Copyright 2019-2026 Mike Fährmann
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License version 2 as
|
||||||
@@ -153,7 +153,7 @@ class TsuminoSearchExtractor(TsuminoBase, Extractor):
|
|||||||
try:
|
try:
|
||||||
if query[0] == "?":
|
if query[0] == "?":
|
||||||
return self._parse_simple(query)
|
return self._parse_simple(query)
|
||||||
return self._parse_jsurl(query)
|
return self.utils("/jsurl").parse(query)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise exception.AbortExtraction(
|
raise exception.AbortExtraction(
|
||||||
f"Invalid search query '{query}' ({exc})")
|
f"Invalid search query '{query}' ({exc})")
|
||||||
@@ -177,128 +177,3 @@ class TsuminoSearchExtractor(TsuminoBase, Extractor):
|
|||||||
"Tags[0][Text]": text.unquote(value).replace("+", " "),
|
"Tags[0][Text]": text.unquote(value).replace("+", " "),
|
||||||
"Tags[0][Exclude]": "false",
|
"Tags[0][Exclude]": "false",
|
||||||
}
|
}
|
||||||
|
|
||||||
def _parse_jsurl(self, data):
|
|
||||||
"""Parse search query in JSURL format
|
|
||||||
|
|
||||||
Nested lists and dicts are handled in a special way to deal
|
|
||||||
with the way Tsumino expects its parameters -> expand(...)
|
|
||||||
|
|
||||||
Example: ~(name~'John*20Doe~age~42~children~(~'Mary~'Bill))
|
|
||||||
Ref: https://github.com/Sage/jsurl
|
|
||||||
"""
|
|
||||||
i = 0
|
|
||||||
imax = len(data)
|
|
||||||
|
|
||||||
def eat(expected):
|
|
||||||
nonlocal i
|
|
||||||
|
|
||||||
if data[i] != expected:
|
|
||||||
raise ValueError(
|
|
||||||
f"bad JSURL syntax: expected '{expected}', got {data[i]}")
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
def decode():
|
|
||||||
nonlocal i
|
|
||||||
|
|
||||||
beg = i
|
|
||||||
result = ""
|
|
||||||
|
|
||||||
while i < imax:
|
|
||||||
ch = data[i]
|
|
||||||
|
|
||||||
if ch not in "~)*!":
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
elif ch == "*":
|
|
||||||
if beg < i:
|
|
||||||
result += data[beg:i]
|
|
||||||
if data[i + 1] == "*":
|
|
||||||
result += chr(int(data[i+2:i+6], 16))
|
|
||||||
i += 6
|
|
||||||
else:
|
|
||||||
result += chr(int(data[i+1:i+3], 16))
|
|
||||||
i += 3
|
|
||||||
beg = i
|
|
||||||
|
|
||||||
elif ch == "!":
|
|
||||||
if beg < i:
|
|
||||||
result += data[beg:i]
|
|
||||||
result += "$"
|
|
||||||
i += 1
|
|
||||||
beg = i
|
|
||||||
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
return result + data[beg:i]
|
|
||||||
|
|
||||||
def parse_one():
|
|
||||||
nonlocal i
|
|
||||||
|
|
||||||
eat('~')
|
|
||||||
result = ""
|
|
||||||
ch = data[i]
|
|
||||||
|
|
||||||
if ch == "(":
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
if data[i] == "~":
|
|
||||||
result = []
|
|
||||||
if data[i+1] == ")":
|
|
||||||
i += 1
|
|
||||||
else:
|
|
||||||
result.append(parse_one())
|
|
||||||
while data[i] == "~":
|
|
||||||
result.append(parse_one())
|
|
||||||
|
|
||||||
else:
|
|
||||||
result = {}
|
|
||||||
|
|
||||||
if data[i] != ")":
|
|
||||||
while True:
|
|
||||||
key = decode()
|
|
||||||
value = parse_one()
|
|
||||||
for ekey, evalue in expand(key, value):
|
|
||||||
result[ekey] = evalue
|
|
||||||
if data[i] != "~":
|
|
||||||
break
|
|
||||||
i += 1
|
|
||||||
eat(")")
|
|
||||||
|
|
||||||
elif ch == "'":
|
|
||||||
i += 1
|
|
||||||
result = decode()
|
|
||||||
|
|
||||||
else:
|
|
||||||
beg = i
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
while i < imax and data[i] not in "~)":
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
sub = data[beg:i]
|
|
||||||
if ch in "0123456789-":
|
|
||||||
fval = float(sub)
|
|
||||||
ival = int(fval)
|
|
||||||
result = ival if ival == fval else fval
|
|
||||||
else:
|
|
||||||
if sub not in ("true", "false", "null"):
|
|
||||||
raise ValueError("bad value keyword: " + sub)
|
|
||||||
result = sub
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def expand(key, value):
|
|
||||||
if isinstance(value, list):
|
|
||||||
for index, cvalue in enumerate(value):
|
|
||||||
ckey = f"{key}[{index}]"
|
|
||||||
yield from expand(ckey, cvalue)
|
|
||||||
elif isinstance(value, dict):
|
|
||||||
for ckey, cvalue in value.items():
|
|
||||||
ckey = f"{key}[{ckey}]"
|
|
||||||
yield from expand(ckey, cvalue)
|
|
||||||
else:
|
|
||||||
yield key, value
|
|
||||||
|
|
||||||
return parse_one()
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright 2016-2025 Mike Fährmann
|
# Copyright 2016-2026 Mike Fährmann
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License version 2 as
|
||||||
@@ -1720,8 +1720,7 @@ class TwitterAPI():
|
|||||||
def _client_transaction(self):
|
def _client_transaction(self):
|
||||||
self.log.info("Initializing client transaction keys")
|
self.log.info("Initializing client transaction keys")
|
||||||
|
|
||||||
from .. import transaction_id
|
ct = self.extractor.utils("transaction_id").ClientTransaction()
|
||||||
ct = transaction_id.ClientTransaction()
|
|
||||||
ct.initialize(self.extractor)
|
ct.initialize(self.extractor)
|
||||||
|
|
||||||
# update 'x-csrf-token' header (#7467)
|
# update 'x-csrf-token' header (#7467)
|
||||||
|
|||||||
512
gallery_dl/extractor/utils/500px_graphql.py
Normal file
512
gallery_dl/extractor/utils/500px_graphql.py
Normal file
@@ -0,0 +1,512 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2026 Mike Fährmann
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
OtherPhotosQuery = """\
|
||||||
|
query OtherPhotosQuery($username: String!, $pageSize: Int) {
|
||||||
|
user: userByUsername(username: $username) {
|
||||||
|
...OtherPhotosPaginationContainer_user_RlXb8
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment OtherPhotosPaginationContainer_user_RlXb8 on User {
|
||||||
|
photos(first: $pageSize, privacy: PROFILE, sort: ID_DESC) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
legacyId
|
||||||
|
canonicalPath
|
||||||
|
width
|
||||||
|
height
|
||||||
|
name
|
||||||
|
isLikedByMe
|
||||||
|
notSafeForWork
|
||||||
|
photographer: uploader {
|
||||||
|
id
|
||||||
|
legacyId
|
||||||
|
username
|
||||||
|
displayName
|
||||||
|
canonicalPath
|
||||||
|
followedByUsers {
|
||||||
|
isFollowedByMe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
images(sizes: [33, 35]) {
|
||||||
|
size
|
||||||
|
url
|
||||||
|
jpegUrl
|
||||||
|
webpUrl
|
||||||
|
id
|
||||||
|
}
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
cursor
|
||||||
|
}
|
||||||
|
totalCount
|
||||||
|
pageInfo {
|
||||||
|
endCursor
|
||||||
|
hasNextPage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
OtherPhotosPaginationContainerQuery = """\
|
||||||
|
query OtherPhotosPaginationContainerQuery($username: String!, $pageSize: Int, $cursor: String) {
|
||||||
|
userByUsername(username: $username) {
|
||||||
|
...OtherPhotosPaginationContainer_user_3e6UuE
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment OtherPhotosPaginationContainer_user_3e6UuE on User {
|
||||||
|
photos(first: $pageSize, after: $cursor, privacy: PROFILE, sort: ID_DESC) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
legacyId
|
||||||
|
canonicalPath
|
||||||
|
width
|
||||||
|
height
|
||||||
|
name
|
||||||
|
isLikedByMe
|
||||||
|
notSafeForWork
|
||||||
|
photographer: uploader {
|
||||||
|
id
|
||||||
|
legacyId
|
||||||
|
username
|
||||||
|
displayName
|
||||||
|
canonicalPath
|
||||||
|
followedByUsers {
|
||||||
|
isFollowedByMe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
images(sizes: [33, 35]) {
|
||||||
|
size
|
||||||
|
url
|
||||||
|
jpegUrl
|
||||||
|
webpUrl
|
||||||
|
id
|
||||||
|
}
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
cursor
|
||||||
|
}
|
||||||
|
totalCount
|
||||||
|
pageInfo {
|
||||||
|
endCursor
|
||||||
|
hasNextPage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
ProfileRendererQuery = """\
|
||||||
|
query ProfileRendererQuery($username: String!) {
|
||||||
|
profile: userByUsername(username: $username) {
|
||||||
|
id
|
||||||
|
legacyId
|
||||||
|
userType: type
|
||||||
|
username
|
||||||
|
firstName
|
||||||
|
displayName
|
||||||
|
registeredAt
|
||||||
|
canonicalPath
|
||||||
|
avatar {
|
||||||
|
...ProfileAvatar_avatar
|
||||||
|
id
|
||||||
|
}
|
||||||
|
userProfile {
|
||||||
|
firstname
|
||||||
|
lastname
|
||||||
|
state
|
||||||
|
country
|
||||||
|
city
|
||||||
|
about
|
||||||
|
id
|
||||||
|
}
|
||||||
|
socialMedia {
|
||||||
|
website
|
||||||
|
twitter
|
||||||
|
instagram
|
||||||
|
facebook
|
||||||
|
id
|
||||||
|
}
|
||||||
|
coverPhotoUrl
|
||||||
|
followedByUsers {
|
||||||
|
totalCount
|
||||||
|
isFollowedByMe
|
||||||
|
}
|
||||||
|
followingUsers {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
membership {
|
||||||
|
expiryDate
|
||||||
|
membershipTier: tier
|
||||||
|
photoUploadQuota
|
||||||
|
refreshPhotoUploadQuotaAt
|
||||||
|
paymentStatus
|
||||||
|
id
|
||||||
|
}
|
||||||
|
profileTabs {
|
||||||
|
tabs {
|
||||||
|
name
|
||||||
|
visible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
...EditCover_cover
|
||||||
|
photoStats {
|
||||||
|
likeCount
|
||||||
|
viewCount
|
||||||
|
}
|
||||||
|
photos(privacy: PROFILE) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
licensingPhotos(status: ACCEPTED) {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
portfolio {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
userDisabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment EditCover_cover on User {
|
||||||
|
coverPhotoUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment ProfileAvatar_avatar on UserAvatar {
|
||||||
|
images(sizes: [MEDIUM, LARGE]) {
|
||||||
|
size
|
||||||
|
url
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
GalleriesDetailQueryRendererQuery = """\
|
||||||
|
query GalleriesDetailQueryRendererQuery($galleryOwnerLegacyId: ID!, $ownerLegacyId: String, $slug: String, $token: String, $pageSize: Int, $gallerySize: Int) {
|
||||||
|
galleries(galleryOwnerLegacyId: $galleryOwnerLegacyId, first: $gallerySize) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
legacyId
|
||||||
|
description
|
||||||
|
name
|
||||||
|
privacy
|
||||||
|
canonicalPath
|
||||||
|
notSafeForWork
|
||||||
|
buttonName
|
||||||
|
externalUrl
|
||||||
|
cover {
|
||||||
|
images(sizes: [35, 33]) {
|
||||||
|
size
|
||||||
|
webpUrl
|
||||||
|
jpegUrl
|
||||||
|
id
|
||||||
|
}
|
||||||
|
id
|
||||||
|
}
|
||||||
|
photos {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gallery: galleryByOwnerIdAndSlugOrToken(ownerLegacyId: $ownerLegacyId, slug: $slug, token: $token) {
|
||||||
|
...GalleriesDetailPaginationContainer_gallery_RlXb8
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment GalleriesDetailPaginationContainer_gallery_RlXb8 on Gallery {
|
||||||
|
id
|
||||||
|
legacyId
|
||||||
|
name
|
||||||
|
privacy
|
||||||
|
notSafeForWork
|
||||||
|
ownPhotosOnly
|
||||||
|
canonicalPath
|
||||||
|
publicSlug
|
||||||
|
lastPublishedAt
|
||||||
|
photosAddedSinceLastPublished
|
||||||
|
reportStatus
|
||||||
|
creator {
|
||||||
|
legacyId
|
||||||
|
id
|
||||||
|
}
|
||||||
|
cover {
|
||||||
|
images(sizes: [33, 32, 36, 2048]) {
|
||||||
|
url
|
||||||
|
size
|
||||||
|
webpUrl
|
||||||
|
id
|
||||||
|
}
|
||||||
|
id
|
||||||
|
}
|
||||||
|
description
|
||||||
|
externalUrl
|
||||||
|
buttonName
|
||||||
|
photos(first: $pageSize) {
|
||||||
|
totalCount
|
||||||
|
edges {
|
||||||
|
cursor
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
legacyId
|
||||||
|
canonicalPath
|
||||||
|
name
|
||||||
|
description
|
||||||
|
category
|
||||||
|
uploadedAt
|
||||||
|
location
|
||||||
|
width
|
||||||
|
height
|
||||||
|
isLikedByMe
|
||||||
|
photographer: uploader {
|
||||||
|
id
|
||||||
|
legacyId
|
||||||
|
username
|
||||||
|
displayName
|
||||||
|
canonicalPath
|
||||||
|
avatar {
|
||||||
|
images(sizes: SMALL) {
|
||||||
|
url
|
||||||
|
id
|
||||||
|
}
|
||||||
|
id
|
||||||
|
}
|
||||||
|
followedByUsers {
|
||||||
|
totalCount
|
||||||
|
isFollowedByMe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
images(sizes: [33, 32]) {
|
||||||
|
size
|
||||||
|
url
|
||||||
|
webpUrl
|
||||||
|
id
|
||||||
|
}
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pageInfo {
|
||||||
|
endCursor
|
||||||
|
hasNextPage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
GalleriesDetailPaginationContainerQuery = """\
|
||||||
|
query GalleriesDetailPaginationContainerQuery($ownerLegacyId: String, $slug: String, $token: String, $pageSize: Int, $cursor: String) {
|
||||||
|
galleryByOwnerIdAndSlugOrToken(ownerLegacyId: $ownerLegacyId, slug: $slug, token: $token) {
|
||||||
|
...GalleriesDetailPaginationContainer_gallery_3e6UuE
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment GalleriesDetailPaginationContainer_gallery_3e6UuE on Gallery {
|
||||||
|
id
|
||||||
|
legacyId
|
||||||
|
name
|
||||||
|
privacy
|
||||||
|
notSafeForWork
|
||||||
|
ownPhotosOnly
|
||||||
|
canonicalPath
|
||||||
|
publicSlug
|
||||||
|
lastPublishedAt
|
||||||
|
photosAddedSinceLastPublished
|
||||||
|
reportStatus
|
||||||
|
creator {
|
||||||
|
legacyId
|
||||||
|
id
|
||||||
|
}
|
||||||
|
cover {
|
||||||
|
images(sizes: [33, 32, 36, 2048]) {
|
||||||
|
url
|
||||||
|
size
|
||||||
|
webpUrl
|
||||||
|
id
|
||||||
|
}
|
||||||
|
id
|
||||||
|
}
|
||||||
|
description
|
||||||
|
externalUrl
|
||||||
|
buttonName
|
||||||
|
photos(first: $pageSize, after: $cursor) {
|
||||||
|
totalCount
|
||||||
|
edges {
|
||||||
|
cursor
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
legacyId
|
||||||
|
canonicalPath
|
||||||
|
name
|
||||||
|
description
|
||||||
|
category
|
||||||
|
uploadedAt
|
||||||
|
location
|
||||||
|
width
|
||||||
|
height
|
||||||
|
isLikedByMe
|
||||||
|
photographer: uploader {
|
||||||
|
id
|
||||||
|
legacyId
|
||||||
|
username
|
||||||
|
displayName
|
||||||
|
canonicalPath
|
||||||
|
avatar {
|
||||||
|
images(sizes: SMALL) {
|
||||||
|
url
|
||||||
|
id
|
||||||
|
}
|
||||||
|
id
|
||||||
|
}
|
||||||
|
followedByUsers {
|
||||||
|
totalCount
|
||||||
|
isFollowedByMe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
images(sizes: [33, 32]) {
|
||||||
|
size
|
||||||
|
url
|
||||||
|
webpUrl
|
||||||
|
id
|
||||||
|
}
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pageInfo {
|
||||||
|
endCursor
|
||||||
|
hasNextPage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
LikedPhotosQueryRendererQuery = """\
|
||||||
|
query LikedPhotosQueryRendererQuery($pageSize: Int) {
|
||||||
|
...LikedPhotosPaginationContainer_query_RlXb8
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment LikedPhotosPaginationContainer_query_RlXb8 on Query {
|
||||||
|
likedPhotos(first: $pageSize) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
legacyId
|
||||||
|
canonicalPath
|
||||||
|
name
|
||||||
|
description
|
||||||
|
category
|
||||||
|
uploadedAt
|
||||||
|
location
|
||||||
|
width
|
||||||
|
height
|
||||||
|
isLikedByMe
|
||||||
|
notSafeForWork
|
||||||
|
tags
|
||||||
|
photographer: uploader {
|
||||||
|
id
|
||||||
|
legacyId
|
||||||
|
username
|
||||||
|
displayName
|
||||||
|
canonicalPath
|
||||||
|
avatar {
|
||||||
|
images {
|
||||||
|
url
|
||||||
|
id
|
||||||
|
}
|
||||||
|
id
|
||||||
|
}
|
||||||
|
followedByUsers {
|
||||||
|
totalCount
|
||||||
|
isFollowedByMe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
images(sizes: [33, 35]) {
|
||||||
|
size
|
||||||
|
url
|
||||||
|
jpegUrl
|
||||||
|
webpUrl
|
||||||
|
id
|
||||||
|
}
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
cursor
|
||||||
|
}
|
||||||
|
pageInfo {
|
||||||
|
endCursor
|
||||||
|
hasNextPage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
LikedPhotosPaginationContainerQuery = """\
|
||||||
|
query LikedPhotosPaginationContainerQuery($cursor: String, $pageSize: Int) {
|
||||||
|
...LikedPhotosPaginationContainer_query_3e6UuE
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment LikedPhotosPaginationContainer_query_3e6UuE on Query {
|
||||||
|
likedPhotos(first: $pageSize, after: $cursor) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
legacyId
|
||||||
|
canonicalPath
|
||||||
|
name
|
||||||
|
description
|
||||||
|
category
|
||||||
|
uploadedAt
|
||||||
|
location
|
||||||
|
width
|
||||||
|
height
|
||||||
|
isLikedByMe
|
||||||
|
notSafeForWork
|
||||||
|
tags
|
||||||
|
photographer: uploader {
|
||||||
|
id
|
||||||
|
legacyId
|
||||||
|
username
|
||||||
|
displayName
|
||||||
|
canonicalPath
|
||||||
|
avatar {
|
||||||
|
images {
|
||||||
|
url
|
||||||
|
id
|
||||||
|
}
|
||||||
|
id
|
||||||
|
}
|
||||||
|
followedByUsers {
|
||||||
|
totalCount
|
||||||
|
isFollowedByMe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
images(sizes: [33, 35]) {
|
||||||
|
size
|
||||||
|
url
|
||||||
|
jpegUrl
|
||||||
|
webpUrl
|
||||||
|
id
|
||||||
|
}
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
cursor
|
||||||
|
}
|
||||||
|
pageInfo {
|
||||||
|
endCursor
|
||||||
|
hasNextPage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
0
gallery_dl/extractor/utils/__init__.py
Normal file
0
gallery_dl/extractor/utils/__init__.py
Normal file
416
gallery_dl/extractor/utils/behance_graphql.py
Normal file
416
gallery_dl/extractor/utils/behance_graphql.py
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2026 Mike Fährmann
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
GetProfileProjects = """\
|
||||||
|
query GetProfileProjects($username: String, $after: String) {
|
||||||
|
user(username: $username) {
|
||||||
|
profileProjects(first: 12, after: $after) {
|
||||||
|
pageInfo {
|
||||||
|
endCursor
|
||||||
|
hasNextPage
|
||||||
|
}
|
||||||
|
nodes {
|
||||||
|
__typename
|
||||||
|
adminFlags {
|
||||||
|
mature_lock
|
||||||
|
privacy_lock
|
||||||
|
dmca_lock
|
||||||
|
flagged_lock
|
||||||
|
privacy_violation_lock
|
||||||
|
trademark_lock
|
||||||
|
spam_lock
|
||||||
|
eu_ip_lock
|
||||||
|
}
|
||||||
|
colors {
|
||||||
|
r
|
||||||
|
g
|
||||||
|
b
|
||||||
|
}
|
||||||
|
covers {
|
||||||
|
size_202 {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
size_404 {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
size_808 {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
features {
|
||||||
|
url
|
||||||
|
name
|
||||||
|
featuredOn
|
||||||
|
ribbon {
|
||||||
|
image
|
||||||
|
image2x
|
||||||
|
image3x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fields {
|
||||||
|
id
|
||||||
|
label
|
||||||
|
slug
|
||||||
|
url
|
||||||
|
}
|
||||||
|
hasMatureContent
|
||||||
|
id
|
||||||
|
isFeatured
|
||||||
|
isHiddenFromWorkTab
|
||||||
|
isMatureReviewSubmitted
|
||||||
|
isOwner
|
||||||
|
isFounder
|
||||||
|
isPinnedToSubscriptionOverview
|
||||||
|
isPrivate
|
||||||
|
linkedAssets {
|
||||||
|
...sourceLinkFields
|
||||||
|
}
|
||||||
|
linkedAssetsCount
|
||||||
|
sourceFiles {
|
||||||
|
...sourceFileFields
|
||||||
|
}
|
||||||
|
matureAccess
|
||||||
|
modifiedOn
|
||||||
|
name
|
||||||
|
owners {
|
||||||
|
...OwnerFields
|
||||||
|
images {
|
||||||
|
size_50 {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
premium
|
||||||
|
publishedOn
|
||||||
|
stats {
|
||||||
|
appreciations {
|
||||||
|
all
|
||||||
|
}
|
||||||
|
views {
|
||||||
|
all
|
||||||
|
}
|
||||||
|
comments {
|
||||||
|
all
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slug
|
||||||
|
tools {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
category
|
||||||
|
categoryLabel
|
||||||
|
categoryId
|
||||||
|
approved
|
||||||
|
url
|
||||||
|
backgroundColor
|
||||||
|
}
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment sourceFileFields on SourceFile {
|
||||||
|
__typename
|
||||||
|
sourceFileId
|
||||||
|
projectId
|
||||||
|
userId
|
||||||
|
title
|
||||||
|
assetId
|
||||||
|
renditionUrl
|
||||||
|
mimeType
|
||||||
|
size
|
||||||
|
category
|
||||||
|
licenseType
|
||||||
|
unitAmount
|
||||||
|
currency
|
||||||
|
tier
|
||||||
|
hidden
|
||||||
|
extension
|
||||||
|
hasUserPurchased
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment sourceLinkFields on LinkedAsset {
|
||||||
|
__typename
|
||||||
|
name
|
||||||
|
premium
|
||||||
|
url
|
||||||
|
category
|
||||||
|
licenseType
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment OwnerFields on User {
|
||||||
|
displayName
|
||||||
|
hasPremiumAccess
|
||||||
|
id
|
||||||
|
isFollowing
|
||||||
|
isProfileOwner
|
||||||
|
location
|
||||||
|
locationUrl
|
||||||
|
url
|
||||||
|
username
|
||||||
|
availabilityInfo {
|
||||||
|
availabilityTimeline
|
||||||
|
isAvailableFullTime
|
||||||
|
isAvailableFreelance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
GetMoodboardItemsAndRecommendations = """\
|
||||||
|
query GetMoodboardItemsAndRecommendations(
|
||||||
|
$id: Int!
|
||||||
|
$firstItem: Int!
|
||||||
|
$afterItem: String
|
||||||
|
$shouldGetRecommendations: Boolean!
|
||||||
|
$shouldGetItems: Boolean!
|
||||||
|
$shouldGetMoodboardFields: Boolean!
|
||||||
|
) {
|
||||||
|
viewer @include(if: $shouldGetMoodboardFields) {
|
||||||
|
isOptedOutOfRecommendations
|
||||||
|
isAdmin
|
||||||
|
}
|
||||||
|
moodboard(id: $id) {
|
||||||
|
...moodboardFields @include(if: $shouldGetMoodboardFields)
|
||||||
|
|
||||||
|
items(first: $firstItem, after: $afterItem) @include(if: $shouldGetItems) {
|
||||||
|
pageInfo {
|
||||||
|
endCursor
|
||||||
|
hasNextPage
|
||||||
|
}
|
||||||
|
nodes {
|
||||||
|
...nodesFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recommendedItems(first: 80) @include(if: $shouldGetRecommendations) {
|
||||||
|
nodes {
|
||||||
|
...nodesFields
|
||||||
|
fetchSource
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment moodboardFields on Moodboard {
|
||||||
|
id
|
||||||
|
label
|
||||||
|
privacy
|
||||||
|
followerCount
|
||||||
|
isFollowing
|
||||||
|
projectCount
|
||||||
|
url
|
||||||
|
isOwner
|
||||||
|
owners {
|
||||||
|
...OwnerFields
|
||||||
|
images {
|
||||||
|
size_50 {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
size_100 {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
size_115 {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
size_230 {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
size_138 {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
size_276 {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment projectFields on Project {
|
||||||
|
__typename
|
||||||
|
id
|
||||||
|
isOwner
|
||||||
|
publishedOn
|
||||||
|
matureAccess
|
||||||
|
hasMatureContent
|
||||||
|
modifiedOn
|
||||||
|
name
|
||||||
|
url
|
||||||
|
isPrivate
|
||||||
|
slug
|
||||||
|
license {
|
||||||
|
license
|
||||||
|
description
|
||||||
|
id
|
||||||
|
label
|
||||||
|
url
|
||||||
|
text
|
||||||
|
images
|
||||||
|
}
|
||||||
|
fields {
|
||||||
|
label
|
||||||
|
}
|
||||||
|
colors {
|
||||||
|
r
|
||||||
|
g
|
||||||
|
b
|
||||||
|
}
|
||||||
|
owners {
|
||||||
|
...OwnerFields
|
||||||
|
images {
|
||||||
|
size_50 {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
size_100 {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
size_115 {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
size_230 {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
size_138 {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
size_276 {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
covers {
|
||||||
|
size_original {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
size_max_808 {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
size_808 {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
size_404 {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
size_202 {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
size_230 {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
size_115 {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stats {
|
||||||
|
views {
|
||||||
|
all
|
||||||
|
}
|
||||||
|
appreciations {
|
||||||
|
all
|
||||||
|
}
|
||||||
|
comments {
|
||||||
|
all
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment exifDataValueFields on exifDataValue {
|
||||||
|
id
|
||||||
|
label
|
||||||
|
value
|
||||||
|
searchValue
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment nodesFields on MoodboardItem {
|
||||||
|
id
|
||||||
|
entityType
|
||||||
|
width
|
||||||
|
height
|
||||||
|
flexWidth
|
||||||
|
flexHeight
|
||||||
|
images {
|
||||||
|
size
|
||||||
|
url
|
||||||
|
}
|
||||||
|
|
||||||
|
entity {
|
||||||
|
... on Project {
|
||||||
|
...projectFields
|
||||||
|
}
|
||||||
|
|
||||||
|
... on ImageModule {
|
||||||
|
project {
|
||||||
|
...projectFields
|
||||||
|
}
|
||||||
|
|
||||||
|
colors {
|
||||||
|
r
|
||||||
|
g
|
||||||
|
b
|
||||||
|
}
|
||||||
|
|
||||||
|
exifData {
|
||||||
|
lens {
|
||||||
|
...exifDataValueFields
|
||||||
|
}
|
||||||
|
software {
|
||||||
|
...exifDataValueFields
|
||||||
|
}
|
||||||
|
makeAndModel {
|
||||||
|
...exifDataValueFields
|
||||||
|
}
|
||||||
|
focalLength {
|
||||||
|
...exifDataValueFields
|
||||||
|
}
|
||||||
|
iso {
|
||||||
|
...exifDataValueFields
|
||||||
|
}
|
||||||
|
location {
|
||||||
|
...exifDataValueFields
|
||||||
|
}
|
||||||
|
flash {
|
||||||
|
...exifDataValueFields
|
||||||
|
}
|
||||||
|
exposureMode {
|
||||||
|
...exifDataValueFields
|
||||||
|
}
|
||||||
|
shutterSpeed {
|
||||||
|
...exifDataValueFields
|
||||||
|
}
|
||||||
|
aperture {
|
||||||
|
...exifDataValueFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
... on MediaCollectionComponent {
|
||||||
|
project {
|
||||||
|
...projectFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment OwnerFields on User {
|
||||||
|
displayName
|
||||||
|
hasPremiumAccess
|
||||||
|
id
|
||||||
|
isFollowing
|
||||||
|
isProfileOwner
|
||||||
|
location
|
||||||
|
locationUrl
|
||||||
|
url
|
||||||
|
username
|
||||||
|
availabilityInfo {
|
||||||
|
availabilityTimeline
|
||||||
|
isAvailableFullTime
|
||||||
|
isAvailableFreelance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
116
gallery_dl/extractor/utils/deviantart_journal.py
Normal file
116
gallery_dl/extractor/utils/deviantart_journal.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2026 Mike Fährmann
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
SHADOW = """
|
||||||
|
<span class="shadow">
|
||||||
|
<img src="{src}" class="smshadow" width="{width}" height="{height}">
|
||||||
|
</span>
|
||||||
|
<br><br>
|
||||||
|
"""
|
||||||
|
|
||||||
|
HEADER = """<div usr class="gr">
|
||||||
|
<div class="metadata">
|
||||||
|
<h2><a href="{url}">{title}</a></h2>
|
||||||
|
<ul>
|
||||||
|
<li class="author">
|
||||||
|
by <span class="name"><span class="username-with-symbol u">
|
||||||
|
<a class="u regular username" href="{userurl}">{username}</a>\
|
||||||
|
<span class="user-symbol regular"></span></span></span>,
|
||||||
|
<span>{date}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
HEADER_CUSTOM = """<div class='boxtop journaltop'>
|
||||||
|
<h2>
|
||||||
|
<img src="https://st.deviantart.net/minish/gruzecontrol/icons/journal.gif\
|
||||||
|
?2" style="vertical-align:middle" alt=""/>
|
||||||
|
<a href="{url}">{title}</a>
|
||||||
|
</h2>
|
||||||
|
Journal Entry: <span>{date}</span>
|
||||||
|
"""
|
||||||
|
|
||||||
|
HTML = """text:<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>{title}</title>
|
||||||
|
<link rel="stylesheet" href="https://st.deviantart.net\
|
||||||
|
/css/deviantart-network_lc.css?3843780832"/>
|
||||||
|
<link rel="stylesheet" href="https://st.deviantart.net\
|
||||||
|
/css/group_secrets_lc.css?3250492874"/>
|
||||||
|
<link rel="stylesheet" href="https://st.deviantart.net\
|
||||||
|
/css/v6core_lc.css?4246581581"/>
|
||||||
|
<link rel="stylesheet" href="https://st.deviantart.net\
|
||||||
|
/css/sidebar_lc.css?1490570941"/>
|
||||||
|
<link rel="stylesheet" href="https://st.deviantart.net\
|
||||||
|
/css/writer_lc.css?3090682151"/>
|
||||||
|
<link rel="stylesheet" href="https://st.deviantart.net\
|
||||||
|
/css/v6loggedin_lc.css?3001430805"/>
|
||||||
|
<style>{css}</style>
|
||||||
|
<link rel="stylesheet" href="https://st.deviantart.net\
|
||||||
|
/roses/cssmin/core.css?1488405371919"/>
|
||||||
|
<link rel="stylesheet" href="https://st.deviantart.net\
|
||||||
|
/roses/cssmin/peeky.css?1487067424177"/>
|
||||||
|
<link rel="stylesheet" href="https://st.deviantart.net\
|
||||||
|
/roses/cssmin/desktop.css?1491362542749"/>
|
||||||
|
<link rel="stylesheet" href="https://static.parastorage.com/services\
|
||||||
|
/da-deviation/2bfd1ff7a9d6bf10d27b98dd8504c0399c3f9974a015785114b7dc6b\
|
||||||
|
/app.min.css"/>
|
||||||
|
</head>
|
||||||
|
<body id="deviantART-v7" class="bubble no-apps loggedout w960 deviantart">
|
||||||
|
<div id="output">
|
||||||
|
<div class="dev-page-container bubbleview">
|
||||||
|
<div class="dev-page-view view-mode-normal">
|
||||||
|
<div class="dev-view-main-content">
|
||||||
|
<div class="dev-view-deviation">
|
||||||
|
{shadow}
|
||||||
|
<div class="journal-wrapper tt-a">
|
||||||
|
<div class="journal-wrapper2">
|
||||||
|
<div class="journal {cls} journalcontrol">
|
||||||
|
{html}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
HTML_EXTRA = """\
|
||||||
|
<div id="devskin0"><div class="negate-box-margin" style="">\
|
||||||
|
<div usr class="gr-box gr-genericbox"
|
||||||
|
><i usr class="gr1"><i></i></i
|
||||||
|
><i usr class="gr2"><i></i></i
|
||||||
|
><i usr class="gr3"><i></i></i
|
||||||
|
><div usr class="gr-top">
|
||||||
|
<i usr class="tri"></i>
|
||||||
|
{}
|
||||||
|
</div>
|
||||||
|
</div><div usr class="gr-body"><div usr class="gr">
|
||||||
|
<div class="grf-indent">
|
||||||
|
<div class="text">
|
||||||
|
{} </div>
|
||||||
|
</div>
|
||||||
|
</div></div>
|
||||||
|
<i usr class="gr3 gb"></i>
|
||||||
|
<i usr class="gr2 gb"></i>
|
||||||
|
<i usr class="gr1 gb gb1"></i> </div>
|
||||||
|
</div></div>"""
|
||||||
|
|
||||||
|
TEXT = """text:{title}
|
||||||
|
by {username}, {date}
|
||||||
|
|
||||||
|
{content}
|
||||||
|
"""
|
||||||
256
gallery_dl/extractor/utils/deviantart_tiptap.py
Normal file
256
gallery_dl/extractor/utils/deviantart_tiptap.py
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2026 Mike Fährmann
|
||||||
|
#
|
||||||
|
# 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 ... import text, util
|
||||||
|
from .. deviantart import eclipse_media
|
||||||
|
|
||||||
|
|
||||||
|
def to_html(markup):
|
||||||
|
html = []
|
||||||
|
|
||||||
|
html.append('<div data-editor-viewer="1" '
|
||||||
|
'class="_83r8m _2CKTq _3NjDa mDnFl">')
|
||||||
|
data = util.json_loads(markup)
|
||||||
|
for block in data["document"]["content"]:
|
||||||
|
process_content(html, block)
|
||||||
|
html.append("</div>")
|
||||||
|
|
||||||
|
return "".join(html)
|
||||||
|
|
||||||
|
|
||||||
|
def process_content(html, content):
|
||||||
|
type = content["type"]
|
||||||
|
|
||||||
|
if type == "paragraph":
|
||||||
|
if children := content.get("content"):
|
||||||
|
html.append('<p style="')
|
||||||
|
|
||||||
|
if attrs := content.get("attrs"):
|
||||||
|
if align := attrs.get("textAlign"):
|
||||||
|
html.append("text-align:")
|
||||||
|
html.append(align)
|
||||||
|
html.append(";")
|
||||||
|
process_indentation(html, attrs)
|
||||||
|
html.append('">')
|
||||||
|
else:
|
||||||
|
html.append('margin-inline-start:0px">')
|
||||||
|
|
||||||
|
for block in children:
|
||||||
|
process_content(html, block)
|
||||||
|
html.append("</p>")
|
||||||
|
else:
|
||||||
|
html.append('<p class="empty-p"><br/></p>')
|
||||||
|
|
||||||
|
elif type == "text":
|
||||||
|
process_text(html, content)
|
||||||
|
|
||||||
|
elif type == "heading":
|
||||||
|
attrs = content["attrs"]
|
||||||
|
level = str(attrs.get("level") or "3")
|
||||||
|
|
||||||
|
html.append("<h")
|
||||||
|
html.append(level)
|
||||||
|
html.append(' style="text-align:')
|
||||||
|
html.append(attrs.get("textAlign") or "left")
|
||||||
|
html.append('">')
|
||||||
|
html.append('<span style="')
|
||||||
|
process_indentation(html, attrs)
|
||||||
|
html.append('">')
|
||||||
|
process_children(html, content)
|
||||||
|
html.append("</span></h")
|
||||||
|
html.append(level)
|
||||||
|
html.append(">")
|
||||||
|
|
||||||
|
elif type in ("listItem", "bulletList", "orderedList", "blockquote"):
|
||||||
|
c = type[1]
|
||||||
|
tag = (
|
||||||
|
"li" if c == "i" else
|
||||||
|
"ul" if c == "u" else
|
||||||
|
"ol" if c == "r" else
|
||||||
|
"blockquote"
|
||||||
|
)
|
||||||
|
html.append("<" + tag + ">")
|
||||||
|
process_children(html, content)
|
||||||
|
html.append("</" + tag + ">")
|
||||||
|
|
||||||
|
elif type == "anchor":
|
||||||
|
attrs = content["attrs"]
|
||||||
|
html.append('<a id="')
|
||||||
|
html.append(attrs.get("id") or "")
|
||||||
|
html.append('" data-testid="anchor"></a>')
|
||||||
|
|
||||||
|
elif type == "hardBreak":
|
||||||
|
html.append("<br/><br/>")
|
||||||
|
|
||||||
|
elif type == "horizontalRule":
|
||||||
|
html.append("<hr/>")
|
||||||
|
|
||||||
|
elif type == "da-deviation":
|
||||||
|
process_deviation(html, content)
|
||||||
|
|
||||||
|
elif type == "da-mention":
|
||||||
|
user = content["attrs"]["user"]["username"]
|
||||||
|
html.append('<a href="https://www.deviantart.com/')
|
||||||
|
html.append(user.lower())
|
||||||
|
html.append('" data-da-type="da-mention" data-user="">@<!-- -->')
|
||||||
|
html.append(user)
|
||||||
|
html.append('</a>')
|
||||||
|
|
||||||
|
elif type == "da-gif":
|
||||||
|
attrs = content["attrs"]
|
||||||
|
width = str(attrs.get("width") or "")
|
||||||
|
height = str(attrs.get("height") or "")
|
||||||
|
url = text.escape(attrs.get("url") or "")
|
||||||
|
|
||||||
|
html.append('<div data-da-type="da-gif" data-width="')
|
||||||
|
html.append(width)
|
||||||
|
html.append('" data-height="')
|
||||||
|
html.append(height)
|
||||||
|
html.append('" data-alignment="')
|
||||||
|
html.append(attrs.get("alignment") or "")
|
||||||
|
html.append('" data-url="')
|
||||||
|
html.append(url)
|
||||||
|
html.append('" class="t61qu"><video role="img" autoPlay="" '
|
||||||
|
'muted="" loop="" style="pointer-events:none" '
|
||||||
|
'controlsList="nofullscreen" playsInline="" '
|
||||||
|
'aria-label="gif" data-da-type="da-gif" width="')
|
||||||
|
html.append(width)
|
||||||
|
html.append('" height="')
|
||||||
|
html.append(height)
|
||||||
|
html.append('" src="')
|
||||||
|
html.append(url)
|
||||||
|
html.append('" class="_1Fkk6"></video></div>')
|
||||||
|
|
||||||
|
elif type == "da-video":
|
||||||
|
src = text.escape(content["attrs"].get("src") or "")
|
||||||
|
html.append('<div data-testid="video" data-da-type="da-video" '
|
||||||
|
'data-src="')
|
||||||
|
html.append(src)
|
||||||
|
html.append('" class="_1Uxvs"><div data-canfs="yes" data-testid="v'
|
||||||
|
'ideo-inner" class="main-video" style="width:780px;hei'
|
||||||
|
'ght:438px"><div style="width:780px;height:438px">'
|
||||||
|
'<video src="')
|
||||||
|
html.append(src)
|
||||||
|
html.append('" style="width:100%;height:100%;" preload="auto" cont'
|
||||||
|
'rols=""></video></div></div></div>')
|
||||||
|
|
||||||
|
else:
|
||||||
|
import logging
|
||||||
|
logging.getLogger("tiptap").warning(
|
||||||
|
"Unsupported content type '%s'", type)
|
||||||
|
|
||||||
|
|
||||||
|
def process_text(html, content):
|
||||||
|
if marks := content.get("marks"):
|
||||||
|
close = []
|
||||||
|
for mark in marks:
|
||||||
|
type = mark["type"]
|
||||||
|
if type == "link":
|
||||||
|
attrs = mark.get("attrs") or {}
|
||||||
|
html.append('<a href="')
|
||||||
|
html.append(text.escape(attrs.get("href") or ""))
|
||||||
|
if "target" in attrs:
|
||||||
|
html.append('" target="')
|
||||||
|
html.append(attrs["target"])
|
||||||
|
html.append('" rel="')
|
||||||
|
html.append(attrs.get("rel") or
|
||||||
|
"noopener noreferrer nofollow ugc")
|
||||||
|
html.append('">')
|
||||||
|
close.append("</a>")
|
||||||
|
elif type == "bold":
|
||||||
|
html.append("<strong>")
|
||||||
|
close.append("</strong>")
|
||||||
|
elif type == "italic":
|
||||||
|
html.append("<em>")
|
||||||
|
close.append("</em>")
|
||||||
|
elif type == "underline":
|
||||||
|
html.append("<u>")
|
||||||
|
close.append("</u>")
|
||||||
|
elif type == "strike":
|
||||||
|
html.append("<s>")
|
||||||
|
close.append("</s>")
|
||||||
|
elif type == "textStyle" and len(mark) <= 1:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
import logging
|
||||||
|
logging.getLogger("tiptap").warning(
|
||||||
|
"Unsupported text marker '%s'", type)
|
||||||
|
close.reverse()
|
||||||
|
html.append(text.escape(content["text"]))
|
||||||
|
html.extend(close)
|
||||||
|
else:
|
||||||
|
html.append(text.escape(content["text"]))
|
||||||
|
|
||||||
|
|
||||||
|
def process_children(html, content):
|
||||||
|
if children := content.get("content"):
|
||||||
|
for block in children:
|
||||||
|
process_content(html, block)
|
||||||
|
|
||||||
|
|
||||||
|
def process_indentation(html, attrs):
|
||||||
|
itype = ("text-indent" if attrs.get("indentType") == "line" else
|
||||||
|
"margin-inline-start")
|
||||||
|
isize = str((attrs.get("indentation") or 0) * 24)
|
||||||
|
html.append(itype + ":" + isize + "px")
|
||||||
|
|
||||||
|
|
||||||
|
def process_deviation(html, content):
|
||||||
|
dev = content["attrs"]["deviation"]
|
||||||
|
media = dev.get("media") or ()
|
||||||
|
|
||||||
|
html.append('<div class="jjNX2">')
|
||||||
|
html.append('<figure class="Qf-HY" data-da-type="da-deviation" '
|
||||||
|
'data-deviation="" '
|
||||||
|
'data-width="" data-link="" data-alignment="center">')
|
||||||
|
|
||||||
|
if "baseUri" in media:
|
||||||
|
url, formats = eclipse_media(media)
|
||||||
|
full = formats["fullview"]
|
||||||
|
|
||||||
|
html.append('<a href="')
|
||||||
|
html.append(text.escape(dev["url"]))
|
||||||
|
html.append('" class="_3ouD5" style="margin:0 auto;display:flex;'
|
||||||
|
'align-items:center;justify-content:center;'
|
||||||
|
'overflow:hidden;width:780px;height:')
|
||||||
|
html.append(str(780 * full["h"] / full["w"]))
|
||||||
|
html.append('px">')
|
||||||
|
|
||||||
|
html.append('<img src="')
|
||||||
|
html.append(text.escape(url))
|
||||||
|
html.append('" alt="')
|
||||||
|
html.append(text.escape(dev["title"]))
|
||||||
|
html.append('" style="width:100%;max-width:100%;display:block"/>')
|
||||||
|
html.append("</a>")
|
||||||
|
|
||||||
|
elif "textContent" in dev:
|
||||||
|
html.append('<div class="_32Hs4" style="width:350px">')
|
||||||
|
|
||||||
|
html.append('<a href="')
|
||||||
|
html.append(text.escape(dev["url"]))
|
||||||
|
html.append('" class="_3ouD5">')
|
||||||
|
|
||||||
|
html.append('''\
|
||||||
|
<section class="Q91qI aG7Yi" style="width:350px;height:313px">\
|
||||||
|
<div class="_16ECM _1xMkk" aria-hidden="true">\
|
||||||
|
<svg height="100%" viewBox="0 0 15 12" preserveAspectRatio="xMidYMin slice" \
|
||||||
|
fill-rule="evenodd">\
|
||||||
|
<linearGradient x1="87.8481761%" y1="16.3690766%" \
|
||||||
|
x2="45.4107524%" y2="71.4898596%" id="app-root-3">\
|
||||||
|
<stop stop-color="#00FF62" offset="0%"></stop>\
|
||||||
|
<stop stop-color="#3197EF" stop-opacity="0" offset="100%"></stop>\
|
||||||
|
</linearGradient>\
|
||||||
|
<text class="_2uqbc" fill="url(#app-root-3)" text-anchor="end" x="15" y="11">J\
|
||||||
|
</text></svg></div><div class="_1xz9u">Literature</div><h3 class="_2WvKD">\
|
||||||
|
''')
|
||||||
|
html.append(text.escape(dev["title"]))
|
||||||
|
html.append('</h3><div class="_2CPLm">')
|
||||||
|
html.append(text.escape(dev["textContent"]["excerpt"]))
|
||||||
|
html.append('</div></section></a></div>')
|
||||||
|
|
||||||
|
html.append('</figure></div>')
|
||||||
133
gallery_dl/extractor/utils/jsurl.py
Normal file
133
gallery_dl/extractor/utils/jsurl.py
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2026 Mike Fährmann
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
def parse(data):
|
||||||
|
"""Parse JSURL data
|
||||||
|
|
||||||
|
Nested lists and dicts are handled in a special way to deal
|
||||||
|
with the way Tsumino expects its parameters -> expand(...)
|
||||||
|
|
||||||
|
Example: ~(name~'John*20Doe~age~42~children~(~'Mary~'Bill))
|
||||||
|
Ref: https://github.com/Sage/jsurl
|
||||||
|
"""
|
||||||
|
i = 0
|
||||||
|
imax = len(data)
|
||||||
|
|
||||||
|
def eat(expected):
|
||||||
|
nonlocal i
|
||||||
|
|
||||||
|
if data[i] != expected:
|
||||||
|
raise ValueError(
|
||||||
|
f"bad JSURL syntax: expected '{expected}', got {data[i]}")
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
def decode():
|
||||||
|
nonlocal i
|
||||||
|
|
||||||
|
beg = i
|
||||||
|
result = ""
|
||||||
|
|
||||||
|
while i < imax:
|
||||||
|
ch = data[i]
|
||||||
|
|
||||||
|
if ch not in "~)*!":
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
elif ch == "*":
|
||||||
|
if beg < i:
|
||||||
|
result += data[beg:i]
|
||||||
|
if data[i + 1] == "*":
|
||||||
|
result += chr(int(data[i+2:i+6], 16))
|
||||||
|
i += 6
|
||||||
|
else:
|
||||||
|
result += chr(int(data[i+1:i+3], 16))
|
||||||
|
i += 3
|
||||||
|
beg = i
|
||||||
|
|
||||||
|
elif ch == "!":
|
||||||
|
if beg < i:
|
||||||
|
result += data[beg:i]
|
||||||
|
result += "$"
|
||||||
|
i += 1
|
||||||
|
beg = i
|
||||||
|
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
return result + data[beg:i]
|
||||||
|
|
||||||
|
def parse_one():
|
||||||
|
nonlocal i
|
||||||
|
|
||||||
|
eat('~')
|
||||||
|
result = ""
|
||||||
|
ch = data[i]
|
||||||
|
|
||||||
|
if ch == "(":
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if data[i] == "~":
|
||||||
|
result = []
|
||||||
|
if data[i+1] == ")":
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
result.append(parse_one())
|
||||||
|
while data[i] == "~":
|
||||||
|
result.append(parse_one())
|
||||||
|
|
||||||
|
else:
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
if data[i] != ")":
|
||||||
|
while True:
|
||||||
|
key = decode()
|
||||||
|
value = parse_one()
|
||||||
|
for ekey, evalue in expand(key, value):
|
||||||
|
result[ekey] = evalue
|
||||||
|
if data[i] != "~":
|
||||||
|
break
|
||||||
|
i += 1
|
||||||
|
eat(")")
|
||||||
|
|
||||||
|
elif ch == "'":
|
||||||
|
i += 1
|
||||||
|
result = decode()
|
||||||
|
|
||||||
|
else:
|
||||||
|
beg = i
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
while i < imax and data[i] not in "~)":
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
sub = data[beg:i]
|
||||||
|
if ch in "0123456789-":
|
||||||
|
fval = float(sub)
|
||||||
|
ival = int(fval)
|
||||||
|
result = ival if ival == fval else fval
|
||||||
|
else:
|
||||||
|
if sub not in ("true", "false", "null"):
|
||||||
|
raise ValueError("bad value keyword: " + sub)
|
||||||
|
result = sub
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def expand(key, value):
|
||||||
|
if isinstance(value, list):
|
||||||
|
for index, cvalue in enumerate(value):
|
||||||
|
ckey = f"{key}[{index}]"
|
||||||
|
yield from expand(ckey, cvalue)
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
for ckey, cvalue in value.items():
|
||||||
|
ckey = f"{key}[{ckey}]"
|
||||||
|
yield from expand(ckey, cvalue)
|
||||||
|
else:
|
||||||
|
yield key, value
|
||||||
|
|
||||||
|
return parse_one()
|
||||||
240
gallery_dl/extractor/utils/luscious_graphql.py
Normal file
240
gallery_dl/extractor/utils/luscious_graphql.py
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2026 Mike Fährmann
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
AlbumGet = """
|
||||||
|
query AlbumGet($id: ID!) {
|
||||||
|
album {
|
||||||
|
get(id: $id) {
|
||||||
|
... on Album {
|
||||||
|
...AlbumStandard
|
||||||
|
}
|
||||||
|
... on MutationError {
|
||||||
|
errors {
|
||||||
|
code
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment AlbumStandard on Album {
|
||||||
|
__typename
|
||||||
|
id
|
||||||
|
title
|
||||||
|
labels
|
||||||
|
description
|
||||||
|
created
|
||||||
|
modified
|
||||||
|
like_status
|
||||||
|
number_of_favorites
|
||||||
|
rating
|
||||||
|
status
|
||||||
|
marked_for_deletion
|
||||||
|
marked_for_processing
|
||||||
|
number_of_pictures
|
||||||
|
number_of_animated_pictures
|
||||||
|
slug
|
||||||
|
is_manga
|
||||||
|
url
|
||||||
|
download_url
|
||||||
|
permissions
|
||||||
|
cover {
|
||||||
|
width
|
||||||
|
height
|
||||||
|
size
|
||||||
|
url
|
||||||
|
}
|
||||||
|
created_by {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
display_name
|
||||||
|
user_title
|
||||||
|
avatar {
|
||||||
|
url
|
||||||
|
size
|
||||||
|
}
|
||||||
|
url
|
||||||
|
}
|
||||||
|
content {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
url
|
||||||
|
}
|
||||||
|
language {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
url
|
||||||
|
}
|
||||||
|
tags {
|
||||||
|
id
|
||||||
|
category
|
||||||
|
text
|
||||||
|
url
|
||||||
|
count
|
||||||
|
}
|
||||||
|
genres {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
slug
|
||||||
|
url
|
||||||
|
}
|
||||||
|
audiences {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
url
|
||||||
|
url
|
||||||
|
}
|
||||||
|
last_viewed_picture {
|
||||||
|
id
|
||||||
|
position
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
AlbumListOwnPictures = """
|
||||||
|
query AlbumListOwnPictures($input: PictureListInput!) {
|
||||||
|
picture {
|
||||||
|
list(input: $input) {
|
||||||
|
info {
|
||||||
|
...FacetCollectionInfo
|
||||||
|
}
|
||||||
|
items {
|
||||||
|
...PictureStandardWithoutAlbum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment FacetCollectionInfo on FacetCollectionInfo {
|
||||||
|
page
|
||||||
|
has_next_page
|
||||||
|
has_previous_page
|
||||||
|
total_items
|
||||||
|
total_pages
|
||||||
|
items_per_page
|
||||||
|
url_complete
|
||||||
|
url_filters_only
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment PictureStandardWithoutAlbum on Picture {
|
||||||
|
__typename
|
||||||
|
id
|
||||||
|
title
|
||||||
|
created
|
||||||
|
like_status
|
||||||
|
number_of_comments
|
||||||
|
number_of_favorites
|
||||||
|
status
|
||||||
|
width
|
||||||
|
height
|
||||||
|
resolution
|
||||||
|
aspect_ratio
|
||||||
|
url_to_original
|
||||||
|
url_to_video
|
||||||
|
is_animated
|
||||||
|
position
|
||||||
|
tags {
|
||||||
|
id
|
||||||
|
category
|
||||||
|
text
|
||||||
|
url
|
||||||
|
}
|
||||||
|
permissions
|
||||||
|
url
|
||||||
|
thumbnails {
|
||||||
|
width
|
||||||
|
height
|
||||||
|
size
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
AlbumListWithPeek = """
|
||||||
|
query AlbumListWithPeek($input: AlbumListInput!) {
|
||||||
|
album {
|
||||||
|
list(input: $input) {
|
||||||
|
info {
|
||||||
|
...FacetCollectionInfo
|
||||||
|
}
|
||||||
|
items {
|
||||||
|
...AlbumMinimal
|
||||||
|
peek_thumbnails {
|
||||||
|
width
|
||||||
|
height
|
||||||
|
size
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment FacetCollectionInfo on FacetCollectionInfo {
|
||||||
|
page
|
||||||
|
has_next_page
|
||||||
|
has_previous_page
|
||||||
|
total_items
|
||||||
|
total_pages
|
||||||
|
items_per_page
|
||||||
|
url_complete
|
||||||
|
url_filters_only
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment AlbumMinimal on Album {
|
||||||
|
__typename
|
||||||
|
id
|
||||||
|
title
|
||||||
|
labels
|
||||||
|
description
|
||||||
|
created
|
||||||
|
modified
|
||||||
|
number_of_favorites
|
||||||
|
number_of_pictures
|
||||||
|
slug
|
||||||
|
is_manga
|
||||||
|
url
|
||||||
|
download_url
|
||||||
|
cover {
|
||||||
|
width
|
||||||
|
height
|
||||||
|
size
|
||||||
|
url
|
||||||
|
}
|
||||||
|
content {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
url
|
||||||
|
}
|
||||||
|
language {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
url
|
||||||
|
}
|
||||||
|
tags {
|
||||||
|
id
|
||||||
|
category
|
||||||
|
text
|
||||||
|
url
|
||||||
|
count
|
||||||
|
}
|
||||||
|
genres {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
slug
|
||||||
|
url
|
||||||
|
}
|
||||||
|
audiences {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
185
gallery_dl/extractor/utils/mangapark_graphql.py
Normal file
185
gallery_dl/extractor/utils/mangapark_graphql.py
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2026 Mike Fährmann
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
Get_comicChapterList = """
|
||||||
|
query Get_comicChapterList($comicId: ID!) {
|
||||||
|
get_comicChapterList(comicId: $comicId) {
|
||||||
|
data {
|
||||||
|
id
|
||||||
|
dname
|
||||||
|
title
|
||||||
|
lang
|
||||||
|
urlPath
|
||||||
|
srcTitle
|
||||||
|
sourceId
|
||||||
|
dateCreate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
Get_chapterNode = """
|
||||||
|
query Get_chapterNode($getChapterNodeId: ID!) {
|
||||||
|
get_chapterNode(id: $getChapterNodeId) {
|
||||||
|
data {
|
||||||
|
id
|
||||||
|
dname
|
||||||
|
lang
|
||||||
|
sourceId
|
||||||
|
srcTitle
|
||||||
|
dateCreate
|
||||||
|
comicNode{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
imageFile {
|
||||||
|
urlList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
Get_comicNode = """
|
||||||
|
query Get_comicNode($getComicNodeId: ID!) {
|
||||||
|
get_comicNode(id: $getComicNodeId) {
|
||||||
|
data {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
artists
|
||||||
|
authors
|
||||||
|
genres
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
get_content_source_chapterList = """
|
||||||
|
query get_content_source_chapterList($sourceId: Int!) {
|
||||||
|
get_content_source_chapterList(
|
||||||
|
sourceId: $sourceId
|
||||||
|
) {
|
||||||
|
|
||||||
|
id
|
||||||
|
data {
|
||||||
|
|
||||||
|
|
||||||
|
id
|
||||||
|
sourceId
|
||||||
|
|
||||||
|
dbStatus
|
||||||
|
isNormal
|
||||||
|
isHidden
|
||||||
|
isDeleted
|
||||||
|
isFinal
|
||||||
|
|
||||||
|
dateCreate
|
||||||
|
datePublic
|
||||||
|
dateModify
|
||||||
|
lang
|
||||||
|
volume
|
||||||
|
serial
|
||||||
|
dname
|
||||||
|
title
|
||||||
|
urlPath
|
||||||
|
|
||||||
|
srcTitle srcColor
|
||||||
|
|
||||||
|
count_images
|
||||||
|
|
||||||
|
stat_count_post_child
|
||||||
|
stat_count_post_reply
|
||||||
|
stat_count_views_login
|
||||||
|
stat_count_views_guest
|
||||||
|
|
||||||
|
userId
|
||||||
|
userNode {
|
||||||
|
|
||||||
|
id
|
||||||
|
data {
|
||||||
|
|
||||||
|
id
|
||||||
|
name
|
||||||
|
uniq
|
||||||
|
avatarUrl
|
||||||
|
urlPath
|
||||||
|
|
||||||
|
verified
|
||||||
|
deleted
|
||||||
|
banned
|
||||||
|
|
||||||
|
dateCreate
|
||||||
|
dateOnline
|
||||||
|
|
||||||
|
stat_count_chapters_normal
|
||||||
|
stat_count_chapters_others
|
||||||
|
|
||||||
|
is_adm is_mod is_vip is_upr
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
disqusId
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
get_content_comic_sources = """
|
||||||
|
query get_content_comic_sources($comicId: Int!, $dbStatuss: [String] = [], $userId: Int, $haveChapter: Boolean, $sortFor: String) {
|
||||||
|
get_content_comic_sources(
|
||||||
|
comicId: $comicId
|
||||||
|
dbStatuss: $dbStatuss
|
||||||
|
userId: $userId
|
||||||
|
haveChapter: $haveChapter
|
||||||
|
sortFor: $sortFor
|
||||||
|
) {
|
||||||
|
|
||||||
|
id
|
||||||
|
data{
|
||||||
|
|
||||||
|
id
|
||||||
|
|
||||||
|
dbStatus
|
||||||
|
isNormal
|
||||||
|
isHidden
|
||||||
|
isDeleted
|
||||||
|
|
||||||
|
lang name altNames authors artists
|
||||||
|
|
||||||
|
release
|
||||||
|
genres summary{code} extraInfo{code}
|
||||||
|
|
||||||
|
urlCover600
|
||||||
|
urlCover300
|
||||||
|
urlCoverOri
|
||||||
|
|
||||||
|
srcTitle srcColor
|
||||||
|
|
||||||
|
chapterCount
|
||||||
|
chapterNode_last {
|
||||||
|
id
|
||||||
|
data {
|
||||||
|
dateCreate datePublic dateModify
|
||||||
|
volume serial
|
||||||
|
dname title
|
||||||
|
urlPath
|
||||||
|
userNode {
|
||||||
|
id data {uniq name}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
144
gallery_dl/extractor/utils/scrolller_graphql.py
Normal file
144
gallery_dl/extractor/utils/scrolller_graphql.py
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2026 Mike Fährmann
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
SubredditPostQuery = """\
|
||||||
|
query SubredditPostQuery(
|
||||||
|
$url: String!
|
||||||
|
) {
|
||||||
|
getPost(
|
||||||
|
data: { url: $url }
|
||||||
|
) {
|
||||||
|
__typename id url title subredditId subredditTitle subredditUrl
|
||||||
|
redditPath isNsfw hasAudio fullLengthSource gfycatSource redgifsSource
|
||||||
|
ownerAvatar username displayName favoriteCount isPaid tags
|
||||||
|
commentsCount commentsRepliesCount isFavorite
|
||||||
|
albumContent { mediaSources { url width height isOptimized } }
|
||||||
|
mediaSources { url width height isOptimized }
|
||||||
|
blurredMediaSources { url width height isOptimized }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
SubredditQuery = """\
|
||||||
|
query SubredditQuery(
|
||||||
|
$url: String!
|
||||||
|
$iterator: String
|
||||||
|
$sortBy: GallerySortBy
|
||||||
|
$filter: GalleryFilter
|
||||||
|
$limit: Int!
|
||||||
|
) {
|
||||||
|
getSubreddit(
|
||||||
|
data: {
|
||||||
|
url: $url,
|
||||||
|
iterator: $iterator,
|
||||||
|
filter: $filter,
|
||||||
|
limit: $limit,
|
||||||
|
sortBy: $sortBy
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
__typename id url title secondaryTitle description createdAt isNsfw
|
||||||
|
subscribers isComplete itemCount videoCount pictureCount albumCount
|
||||||
|
isPaid username tags isFollowing
|
||||||
|
banner { url width height isOptimized }
|
||||||
|
children {
|
||||||
|
iterator items {
|
||||||
|
__typename id url title subredditId subredditTitle subredditUrl
|
||||||
|
redditPath isNsfw hasAudio fullLengthSource gfycatSource
|
||||||
|
redgifsSource ownerAvatar username displayName favoriteCount
|
||||||
|
isPaid tags commentsCount commentsRepliesCount isFavorite
|
||||||
|
albumContent { mediaSources { url width height isOptimized } }
|
||||||
|
mediaSources { url width height isOptimized }
|
||||||
|
blurredMediaSources { url width height isOptimized }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
SubredditChildrenQuery = """\
|
||||||
|
query SubredditChildrenQuery(
|
||||||
|
$subredditId: Int!
|
||||||
|
$iterator: String
|
||||||
|
$filter: GalleryFilter
|
||||||
|
$sortBy: GallerySortBy
|
||||||
|
$limit: Int!
|
||||||
|
$isNsfw: Boolean
|
||||||
|
) {
|
||||||
|
getSubredditChildren(
|
||||||
|
data: {
|
||||||
|
subredditId: $subredditId,
|
||||||
|
iterator: $iterator,
|
||||||
|
filter: $filter,
|
||||||
|
sortBy: $sortBy,
|
||||||
|
limit: $limit,
|
||||||
|
isNsfw: $isNsfw
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
iterator items {
|
||||||
|
__typename id url title subredditId subredditTitle subredditUrl
|
||||||
|
redditPath isNsfw hasAudio fullLengthSource gfycatSource
|
||||||
|
redgifsSource ownerAvatar username displayName favoriteCount isPaid
|
||||||
|
tags commentsCount commentsRepliesCount isFavorite
|
||||||
|
albumContent { mediaSources { url width height isOptimized } }
|
||||||
|
mediaSources { url width height isOptimized }
|
||||||
|
blurredMediaSources { url width height isOptimized }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
GetFollowingSubreddits = """\
|
||||||
|
query GetFollowingSubreddits(
|
||||||
|
$iterator: String,
|
||||||
|
$limit: Int!,
|
||||||
|
$filter: GalleryFilter,
|
||||||
|
$isNsfw: Boolean,
|
||||||
|
$sortBy: GallerySortBy
|
||||||
|
) {
|
||||||
|
getFollowingSubreddits(
|
||||||
|
data: {
|
||||||
|
isNsfw: $isNsfw
|
||||||
|
limit: $limit
|
||||||
|
filter: $filter
|
||||||
|
iterator: $iterator
|
||||||
|
sortBy: $sortBy
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
iterator items {
|
||||||
|
__typename id url title secondaryTitle description createdAt isNsfw
|
||||||
|
subscribers isComplete itemCount videoCount pictureCount albumCount
|
||||||
|
isFollowing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
LoginQuery = """\
|
||||||
|
query LoginQuery(
|
||||||
|
$username: String!,
|
||||||
|
$password: String!
|
||||||
|
) {
|
||||||
|
login(
|
||||||
|
username: $username,
|
||||||
|
password: $password
|
||||||
|
) {
|
||||||
|
username token expiresAt isAdmin status isPremium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
ItemTypeQuery = """\
|
||||||
|
query ItemTypeQuery(
|
||||||
|
$url: String!
|
||||||
|
) {
|
||||||
|
getItemType(
|
||||||
|
url: $url
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"""
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright 2025 Mike Fährmann
|
# Copyright 2025-2026 Mike Fährmann
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License version 2 as
|
||||||
@@ -22,8 +22,8 @@ import random
|
|||||||
import hashlib
|
import hashlib
|
||||||
import binascii
|
import binascii
|
||||||
import itertools
|
import itertools
|
||||||
from . import text, util
|
from ... import text, util
|
||||||
from .cache import cache
|
from ...cache import cache
|
||||||
|
|
||||||
|
|
||||||
class ClientTransaction():
|
class ClientTransaction():
|
||||||
@@ -1,11 +1,20 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from gallery_dl import extractor, downloader, postprocessor
|
from gallery_dl import extractor, downloader, postprocessor
|
||||||
|
import os
|
||||||
|
|
||||||
hiddenimports = [
|
hiddenimports = [
|
||||||
package.__name__ + "." + module
|
f"{package.__name__}.{module}"
|
||||||
for package in (extractor, downloader, postprocessor)
|
for package in (extractor, downloader, postprocessor)
|
||||||
for module in package.modules
|
for module in package.modules
|
||||||
]
|
]
|
||||||
|
|
||||||
|
base = extractor.__name__ + ".utils."
|
||||||
|
path = os.path.join(extractor.__path__[0], "utils")
|
||||||
|
hiddenimports.extend(
|
||||||
|
base + file[:-3]
|
||||||
|
for file in os.listdir(path)
|
||||||
|
if not file.startswith("__")
|
||||||
|
)
|
||||||
|
|
||||||
hiddenimports.append("yt_dlp")
|
hiddenimports.append("yt_dlp")
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ exclude = .git,__pycache__,build,dist,archive
|
|||||||
ignore = E203,E226,W504
|
ignore = E203,E226,W504
|
||||||
per-file-ignores =
|
per-file-ignores =
|
||||||
setup.py: E501
|
setup.py: E501
|
||||||
gallery_dl/extractor/500px.py: E501
|
gallery_dl/extractor/utils/500px_graphql.py: E501
|
||||||
gallery_dl/extractor/mangapark.py: E501
|
gallery_dl/extractor/utils/mangapark_graphql.py: E501
|
||||||
test/results/*.py: E122,E241,E402,E501
|
test/results/*.py: E122,E241,E402,E501
|
||||||
|
|||||||
Reference in New Issue
Block a user