Merge branch 'res'
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# -*- 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
|
||||
# it under the terms of the GNU General Public License version 2 as
|
||||
@@ -83,7 +83,7 @@ class _500pxExtractor(Extractor):
|
||||
data = {
|
||||
"operationName": opname,
|
||||
"variables" : util.json_dumps(variables),
|
||||
"query" : QUERIES[opname],
|
||||
"query" : self.utils("graphql", opname),
|
||||
}
|
||||
return self.request_json(
|
||||
url, method="POST", headers=headers, json=data)["data"]
|
||||
@@ -212,512 +212,3 @@ class _500pxImageExtractor(_500pxExtractor):
|
||||
def photos(self):
|
||||
edges = ({"node": {"legacyId": self.photo_id}},)
|
||||
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 -*-
|
||||
|
||||
# Copyright 2018-2025 Mike Fährmann
|
||||
# Copyright 2018-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
|
||||
@@ -42,7 +42,7 @@ class BehanceExtractor(Extractor):
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
}
|
||||
data = {
|
||||
"query" : GRAPHQL_QUERIES[endpoint],
|
||||
"query" : self.utils("graphql", endpoint),
|
||||
"variables": variables,
|
||||
}
|
||||
|
||||
@@ -284,415 +284,3 @@ class BehanceCollectionExtractor(BehanceExtractor):
|
||||
if not items["pageInfo"]["hasNextPage"]:
|
||||
return
|
||||
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)
|
||||
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):
|
||||
self._check_input_allowed(prompt)
|
||||
|
||||
@@ -1128,6 +1139,7 @@ def _browser_useragent(browser):
|
||||
|
||||
CACHE_ADAPTERS = {}
|
||||
CACHE_COOKIES = {}
|
||||
CACHE_UTILS = {}
|
||||
CATEGORY_MAP = ()
|
||||
|
||||
|
||||
|
||||
@@ -292,7 +292,8 @@ class DeviantartExtractor(Extractor):
|
||||
url = deviation["url"]
|
||||
thumbs = deviation.get("thumbs") or deviation.get("files")
|
||||
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:
|
||||
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:
|
||||
needle = '<div class="boxtop journaltop">'
|
||||
header = HEADER_CUSTOM_TEMPLATE.format(
|
||||
header = tmpl.HEADER_CUSTOM.format(
|
||||
title=title, url=url, date=deviation["date"],
|
||||
)
|
||||
else:
|
||||
needle = '<div usr class="gr">'
|
||||
username = deviation["author"]["username"]
|
||||
urlname = deviation.get("username") or username.lower()
|
||||
header = HEADER_TEMPLATE.format(
|
||||
header = tmpl.HEADER.format(
|
||||
title=title,
|
||||
url=url,
|
||||
userurl=f"{self.root}/{urlname}/",
|
||||
@@ -326,9 +327,9 @@ class DeviantartExtractor(Extractor):
|
||||
if needle in html:
|
||||
html = html.replace(needle, header, 1)
|
||||
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)
|
||||
|
||||
deviation["extension"] = "htm"
|
||||
@@ -345,7 +346,7 @@ class DeviantartExtractor(Extractor):
|
||||
text.unescape(text.remove_html(txt))
|
||||
for txt in (head or tail).split("<br />")
|
||||
)
|
||||
txt = JOURNAL_TEMPLATE_TEXT.format(
|
||||
txt = self.utils("journal").TEXT.format(
|
||||
title=deviation["title"],
|
||||
username=deviation["author"]["username"],
|
||||
date=deviation["date"],
|
||||
@@ -402,7 +403,7 @@ class DeviantartExtractor(Extractor):
|
||||
|
||||
if html["type"] == "tiptap":
|
||||
try:
|
||||
return self._tiptap_to_html(markup)
|
||||
return self.utils("tiptap").to_html(markup)
|
||||
except Exception as exc:
|
||||
self.log.traceback(exc)
|
||||
self.log.error("%s: '%s: %s'", deviation["index"],
|
||||
@@ -411,242 +412,6 @@ class DeviantartExtractor(Extractor):
|
||||
self.log.warning("%s: Unsupported '%s' markup.",
|
||||
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):
|
||||
content = deviation["content"]
|
||||
|
||||
@@ -827,25 +592,6 @@ x2="45.4107524%" y2="71.4898596%" id="app-root-3">\
|
||||
self.log.info("Unwatching %s", 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):
|
||||
for obj in deviations:
|
||||
deviation = obj["deviation"] if "deviation" in obj else obj
|
||||
@@ -1303,7 +1049,7 @@ class DeviantartDeviationExtractor(DeviantartExtractor):
|
||||
yield deviation
|
||||
|
||||
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["num"] += 1
|
||||
deviation["index_file"] = post["fileId"]
|
||||
@@ -2124,6 +1870,9 @@ def _login_impl(extr, username, password):
|
||||
}
|
||||
|
||||
|
||||
_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
|
||||
def id_from_base36(base36):
|
||||
return util.bdecode(base36, _ALPHABET)
|
||||
|
||||
@@ -2132,116 +1881,21 @@ def base36_from_id(deviation_id):
|
||||
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"]
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Journal Formats #############################################################
|
||||
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])
|
||||
|
||||
SHADOW_TEMPLATE = """
|
||||
<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}
|
||||
"""
|
||||
return "".join(url), formats
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- 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
|
||||
# it under the terms of the GNU General Public License version 2 as
|
||||
@@ -18,15 +18,15 @@ class LusciousExtractor(Extractor):
|
||||
cookies_domain = ".luscious.net"
|
||||
root = "https://members.luscious.net"
|
||||
|
||||
def _graphql(self, op, variables, query):
|
||||
def _request_graphql(self, opname, variables):
|
||||
data = {
|
||||
"id" : 1,
|
||||
"operationName": op,
|
||||
"query" : query,
|
||||
"operationName": opname,
|
||||
"query" : self.utils("graphql", opname),
|
||||
"variables" : variables,
|
||||
}
|
||||
response = self.request(
|
||||
f"{self.root}/graphql/nobatch/?operationName={op}",
|
||||
f"{self.root}/graphql/nobatch/?operationName={opname}",
|
||||
method="POST", json=data, fatal=False,
|
||||
)
|
||||
|
||||
@@ -49,11 +49,8 @@ class LusciousAlbumExtractor(LusciousExtractor):
|
||||
r"/(?:albums|pictures/c/[^/?#]+/album)/[^/?#]+_(\d+)")
|
||||
example = "https://luscious.net/albums/TITLE_12345/"
|
||||
|
||||
def __init__(self, match):
|
||||
LusciousExtractor.__init__(self, match)
|
||||
self.album_id = match[1]
|
||||
|
||||
def _init(self):
|
||||
self.album_id = self.groups[0]
|
||||
self.gif = self.config("gif", False)
|
||||
|
||||
def items(self):
|
||||
@@ -83,98 +80,7 @@ class LusciousAlbumExtractor(LusciousExtractor):
|
||||
"id": self.album_id,
|
||||
}
|
||||
|
||||
query = """
|
||||
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"]
|
||||
album = self._request_graphql("AlbumGet", variables)["album"]["get"]
|
||||
if "errors" in 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:
|
||||
data = self._graphql("AlbumListOwnPictures", variables, query)
|
||||
data = self._request_graphql("AlbumListOwnPictures", variables)
|
||||
yield from data["picture"]["list"]["items"]
|
||||
|
||||
if not data["picture"]["list"]["info"]["has_next_page"]:
|
||||
@@ -278,12 +126,8 @@ class LusciousSearchExtractor(LusciousExtractor):
|
||||
r"/albums/list/?(?:\?([^#]+))?")
|
||||
example = "https://luscious.net/albums/list/?tagged=TAG"
|
||||
|
||||
def __init__(self, match):
|
||||
LusciousExtractor.__init__(self, match)
|
||||
self.query = match[1]
|
||||
|
||||
def items(self):
|
||||
query = text.parse_query(self.query)
|
||||
query = text.parse_query(self.groups[0])
|
||||
display = query.pop("display", "date_newest")
|
||||
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:
|
||||
data = self._graphql("AlbumListWithPeek", variables, query)
|
||||
data = self._request_graphql("AlbumListWithPeek", variables)
|
||||
|
||||
for album in data["album"]["list"]["items"]:
|
||||
album["url"] = self.root + album["url"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- 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
|
||||
# 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):
|
||||
url = self.root + "/apo/"
|
||||
data = {
|
||||
"query" : QUERIES[opname],
|
||||
"query" : self.utils("graphql", opname),
|
||||
"variables" : variables,
|
||||
"operationName": opname,
|
||||
}
|
||||
@@ -177,183 +177,3 @@ class MangaparkMangaExtractor(MangaparkBase, Extractor):
|
||||
|
||||
raise exception.AbortExtraction(
|
||||
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",
|
||||
}
|
||||
data = {
|
||||
"query" : QUERIES[opname],
|
||||
"query" : self.utils("graphql", opname),
|
||||
"variables" : variables,
|
||||
"authorization": self.auth_token,
|
||||
}
|
||||
@@ -206,144 +206,3 @@ class ScrolllerPostExtractor(ScrolllerExtractor):
|
||||
variables = {"url": "/" + self.groups[0]}
|
||||
data = self._request_graphql("SubredditPostQuery", variables)
|
||||
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 -*-
|
||||
|
||||
# Copyright 2019-2025 Mike Fährmann
|
||||
# Copyright 2019-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
|
||||
@@ -153,7 +153,7 @@ class TsuminoSearchExtractor(TsuminoBase, Extractor):
|
||||
try:
|
||||
if query[0] == "?":
|
||||
return self._parse_simple(query)
|
||||
return self._parse_jsurl(query)
|
||||
return self.utils("/jsurl").parse(query)
|
||||
except Exception as exc:
|
||||
raise exception.AbortExtraction(
|
||||
f"Invalid search query '{query}' ({exc})")
|
||||
@@ -177,128 +177,3 @@ class TsuminoSearchExtractor(TsuminoBase, Extractor):
|
||||
"Tags[0][Text]": text.unquote(value).replace("+", " "),
|
||||
"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 -*-
|
||||
|
||||
# Copyright 2016-2025 Mike Fährmann
|
||||
# Copyright 2016-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
|
||||
@@ -1720,8 +1720,7 @@ class TwitterAPI():
|
||||
def _client_transaction(self):
|
||||
self.log.info("Initializing client transaction keys")
|
||||
|
||||
from .. import transaction_id
|
||||
ct = transaction_id.ClientTransaction()
|
||||
ct = self.extractor.utils("transaction_id").ClientTransaction()
|
||||
ct.initialize(self.extractor)
|
||||
|
||||
# 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 -*-
|
||||
|
||||
# Copyright 2025 Mike Fährmann
|
||||
# Copyright 2025-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
|
||||
@@ -22,8 +22,8 @@ import random
|
||||
import hashlib
|
||||
import binascii
|
||||
import itertools
|
||||
from . import text, util
|
||||
from .cache import cache
|
||||
from ... import text, util
|
||||
from ...cache import cache
|
||||
|
||||
|
||||
class ClientTransaction():
|
||||
@@ -1,11 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from gallery_dl import extractor, downloader, postprocessor
|
||||
import os
|
||||
|
||||
hiddenimports = [
|
||||
package.__name__ + "." + module
|
||||
f"{package.__name__}.{module}"
|
||||
for package in (extractor, downloader, postprocessor)
|
||||
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")
|
||||
|
||||
@@ -3,6 +3,6 @@ exclude = .git,__pycache__,build,dist,archive
|
||||
ignore = E203,E226,W504
|
||||
per-file-ignores =
|
||||
setup.py: E501
|
||||
gallery_dl/extractor/500px.py: E501
|
||||
gallery_dl/extractor/mangapark.py: E501
|
||||
gallery_dl/extractor/utils/500px_graphql.py: E501
|
||||
gallery_dl/extractor/utils/mangapark_graphql.py: E501
|
||||
test/results/*.py: E122,E241,E402,E501
|
||||
|
||||
Reference in New Issue
Block a user