diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 74d78e2bb..719dac0f4 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -32,8 +32,7 @@ jobs: DANBOORU_AWS_SQS_ENABLED: false DANBOORU_TWITTER_API_KEY: ${{ secrets.DANBOORU_TWITTER_API_KEY }} DANBOORU_TWITTER_API_SECRET: ${{ secrets.DANBOORU_TWITTER_API_SECRET }} - DANBOORU_PIXIV_LOGIN: ${{ secrets.DANBOORU_PIXIV_LOGIN }} - DANBOORU_PIXIV_PASSWORD: ${{ secrets.DANBOORU_PIXIV_PASSWORD }} + DANBOORU_PIXIV_PHPSESSID: ${{ secrets.DANBOORU_PIXIV_PHPSESSID }} DANBOORU_NIJIE_LOGIN: ${{ secrets.DANBOORU_NIJIE_LOGIN }} DANBOORU_NIJIE_PASSWORD: ${{ secrets.DANBOORU_NIJIE_PASSWORD }} DANBOORU_NICO_SEIGA_LOGIN: ${{ secrets.DANBOORU_NICO_SEIGA_LOGIN }} diff --git a/app/logical/pixiv_ajax_client.rb b/app/logical/pixiv_ajax_client.rb new file mode 100644 index 000000000..9b79525c9 --- /dev/null +++ b/app/logical/pixiv_ajax_client.rb @@ -0,0 +1,393 @@ +# Notes on Pixiv's AJAX API: +# +# * The user agent must be spoofed as a browser user agent, otherwise the API +# will return an error. +# * You must authenticate by passing a PHPSESSID cookie, otherwise the API will +# return nothing for R-18 works. +# * The PHPSESSID cookie is obtained by logging in manually and getting the +# cookie from the devtools. +# * The /ajax/illust/$id endpoint only returns the first image for multi-image +# works. The /ajax/illust/$id/pages API must be used to get the full list of images. +# * Ugoiras need to use the /ajax/illust/$id/ugoira_meta API to get the frame metadata. +# +# Endpoints: +# +# * https://www.pixiv.net/ajax/illust/87598468 +# * https://www.pixiv.net/ajax/user/883091/illusts?ids[]=87598468 +# * https://www.pixiv.net/ajax/illust/87598468/pages +# * https://www.pixiv.net/ajax/illust/74932152/ugoira_meta +# +# https://www.pixiv.net/ajax/illust/87598468 +# +# => { +# "error": false, +# "message": "", +# "body": { +# "illustId": "87598468", +# "illustTitle": "Billow", +# "illustComment": "須田景凪さんの1st Full Album「Billow」のイラストを担当いたしました。
ジャケットはデザイナーさんがイラストをぐにゃっとして作ってくれました。

クロスフェード
・niconico
https://www.nicovideo.jp/watch/sm38153587
・youtube
https://www.youtube.com/watch?v=TQnwTiFXSgc

アートディレクター
クロスフェード制作
吉良進太郎", +# "id": "87598468", +# "title": "Billow", +# "description": "須田景凪さんの1st Full Album「Billow」のイラストを担当いたしました。
ジャケットはデザイナーさんがイラストをぐにゃっとして作ってくれました。

クロスフェード
・niconico
https://www.nicovideo.jp/watch/sm38153587
・youtube
https://www.youtube.com/watch?v=TQnwTiFXSgc

アートディレクター
クロスフェード制作
吉良進太郎", +# "illustType": 0, +# "createDate": "2021-02-07T09:02:03+00:00", +# "uploadDate": "2021-02-07T09:02:03+00:00", +# "restrict": 0, +# "xRestrict": 0, +# "sl": 2, +# "urls": { +# "mini": "https://i.pximg.net/c/48x48/img-master/img/2021/02/07/18/02/03/87598468_p0_square1200.jpg", +# "thumb": "https://i.pximg.net/c/250x250_80_a2/img-master/img/2021/02/07/18/02/03/87598468_p0_square1200.jpg", +# "small": "https://i.pximg.net/c/540x540_70/img-master/img/2021/02/07/18/02/03/87598468_p0_master1200.jpg", +# "regular": "https://i.pximg.net/img-master/img/2021/02/07/18/02/03/87598468_p0_master1200.jpg", +# "original": "https://i.pximg.net/img-original/img/2021/02/07/18/02/03/87598468_p0.jpg" +# }, +# "tags": { +# "authorId": "883091", +# "isLocked": false, +# "tags": [ +# { +# "tag": "オリジナル", +# "locked": true, +# "deletable": false, +# "userId": "883091", +# "romaji": "orijinaru", +# "translation": { +# "en": "original" +# }, +# "userName": "アボガド6" +# }, +# { +# "tag": "須田景凪", +# "locked": true, +# "deletable": false, +# "userId": "883091", +# "romaji": "sudakeina", +# "userName": "アボガド6" +# } +# ], +# "writable": true +# }, +# "alt": "original / Billow / February 7th, 2021", +# "storableTags": [ +# "RTJMXD26Ak", +# "bG5P5jsGQR" +# ], +# "userId": "883091", +# "userName": "アボガド6", +# "userAccount": "avogado602", +# "userIllusts": { +# ... +# }, +# "likeData": false, +# "width": 4491, +# "height": 4435, +# "pageCount": 16, +# "bookmarkCount": 3843, +# "likeCount": 3386, +# "commentCount": 11, +# "responseCount": 0, +# "viewCount": 62936, +# "isHowto": false, +# "isOriginal": true, +# "imageResponseOutData": [], +# "imageResponseData": [], +# "imageResponseCount": 0, +# "pollData": null, +# "seriesNavData": null, +# "descriptionBoothId": null, +# "descriptionYoutubeId": "TQnwTiFXSgc", +# "comicPromotion": null, +# "fanboxPromotion": null, +# "contestBanners": [], +# "isBookmarkable": true, +# "bookmarkData": null, +# "contestData": null, +# "zoneConfig": { +# ... +# }, +# "extraData": { +# "meta": { +# "title": "original / Billow / February 7th, 2021 - pixiv", +# "description": "この作品 「Billow」 は 「オリジナル」「須田景凪」 のタグがつけられた「アボガド6」さんのイラストです。 「須田景凪さんの1st Full Album「Billow」のイラストを担当いたしました。ジャケットはデザイナーさんがイラストをぐにゃっとして作ってくれました。クロ…", +# "canonical": "https://www.pixiv.net/en/artworks/87598468", +# "alternateLanguages": { +# "ja": "https://www.pixiv.net/artworks/87598468", +# "en": "https://www.pixiv.net/en/artworks/87598468" +# }, +# "descriptionHeader": "original are the most prominent tags for this work posted on February 7th, 2021.", +# "ogp": { +# "description": "須田景凪さんの1st Full Album「Billow」のイラストを担当いたしました。ジャケットはデザイナーさんがイラストをぐにゃっとして作ってくれました。クロスフェード・niconicohttps", +# "image": "https://embed.pixiv.net/decorate.php?illust_id=87598468", +# "title": "original / Billow / February 7th, 2021 - pixiv", +# "type": "article" +# }, +# "twitter": { +# "description": "須田景凪さんの1st Full Album「Billow」のイラストを担当いたしました。\r\nジャケットはデザイナーさんがイラストをぐにゃっとして作ってくれました。\r\n\r\nクロスフェード\r\n・niconico\r\nhttps://www.nicovideo.jp/watch/sm38153587\r\n・youtube\r\nhttps://www.youtube.com/watch?v=TQnwTiFXSgc\r\n\r\nアートディレクター\r\nクロスフェード制作\r\n吉良進太郎", +# "image": "https://embed.pixiv.net/decorate.php?illust_id=87598468", +# "title": "Billow", +# "card": "summary_large_image" +# } +# } +# }, +# "titleCaptionTranslation": { +# "workTitle": "", +# "workCaption": "" +# }, +# "isUnlisted": false, +# "request": null +# } +# } +# +# https://www.pixiv.net/ajax/illust/87598468/pages?lang=en +# +# => { +# "error": false, +# "message": "", +# "body": [ +# { +# "urls": { +# "thumb_mini": "https://i.pximg.net/c/128x128/img-master/img/2021/02/07/18/02/03/87598468_p0_square1200.jpg", +# "small": "https://i.pximg.net/c/540x540_70/img-master/img/2021/02/07/18/02/03/87598468_p0_master1200.jpg", +# "regular": "https://i.pximg.net/img-master/img/2021/02/07/18/02/03/87598468_p0_master1200.jpg", +# "original": "https://i.pximg.net/img-original/img/2021/02/07/18/02/03/87598468_p0.jpg" +# }, +# "width": 4491, +# "height": 4435 +# }, +# { +# "urls": { +# "thumb_mini": "https://i.pximg.net/c/128x128/img-master/img/2021/02/07/18/02/03/87598468_p1_square1200.jpg", +# "small": "https://i.pximg.net/c/540x540_70/img-master/img/2021/02/07/18/02/03/87598468_p1_master1200.jpg", +# "regular": "https://i.pximg.net/img-master/img/2021/02/07/18/02/03/87598468_p1_master1200.jpg", +# "original": "https://i.pximg.net/img-original/img/2021/02/07/18/02/03/87598468_p1.jpg" +# }, +# "width": 5000, +# "height": 5000 +# }, +# { +# "urls": { +# "thumb_mini": "https://i.pximg.net/c/128x128/img-master/img/2021/02/07/18/02/03/87598468_p2_square1200.jpg", +# "small": "https://i.pximg.net/c/540x540_70/img-master/img/2021/02/07/18/02/03/87598468_p2_master1200.jpg", +# "regular": "https://i.pximg.net/img-master/img/2021/02/07/18/02/03/87598468_p2_master1200.jpg", +# "original": "https://i.pximg.net/img-original/img/2021/02/07/18/02/03/87598468_p2.jpg" +# }, +# "width": 5000, +# "height": 5000 +# }, +# { +# "urls": { +# "thumb_mini": "https://i.pximg.net/c/128x128/img-master/img/2021/02/07/18/02/03/87598468_p3_square1200.jpg", +# "small": "https://i.pximg.net/c/540x540_70/img-master/img/2021/02/07/18/02/03/87598468_p3_master1200.jpg", +# "regular": "https://i.pximg.net/img-master/img/2021/02/07/18/02/03/87598468_p3_master1200.jpg", +# "original": "https://i.pximg.net/img-original/img/2021/02/07/18/02/03/87598468_p3.jpg" +# }, +# "width": 5000, +# "height": 5000 +# }, +# { +# "urls": { +# "thumb_mini": "https://i.pximg.net/c/128x128/img-master/img/2021/02/07/18/02/03/87598468_p4_square1200.jpg", +# "small": "https://i.pximg.net/c/540x540_70/img-master/img/2021/02/07/18/02/03/87598468_p4_master1200.jpg", +# "regular": "https://i.pximg.net/img-master/img/2021/02/07/18/02/03/87598468_p4_master1200.jpg", +# "original": "https://i.pximg.net/img-original/img/2021/02/07/18/02/03/87598468_p4.jpg" +# }, +# "width": 5000, +# "height": 5000 +# }, +# { +# "urls": { +# "thumb_mini": "https://i.pximg.net/c/128x128/img-master/img/2021/02/07/18/02/03/87598468_p5_square1200.jpg", +# "small": "https://i.pximg.net/c/540x540_70/img-master/img/2021/02/07/18/02/03/87598468_p5_master1200.jpg", +# "regular": "https://i.pximg.net/img-master/img/2021/02/07/18/02/03/87598468_p5_master1200.jpg", +# "original": "https://i.pximg.net/img-original/img/2021/02/07/18/02/03/87598468_p5.jpg" +# }, +# "width": 5000, +# "height": 5000 +# }, +# { +# "urls": { +# "thumb_mini": "https://i.pximg.net/c/128x128/img-master/img/2021/02/07/18/02/03/87598468_p6_square1200.jpg", +# "small": "https://i.pximg.net/c/540x540_70/img-master/img/2021/02/07/18/02/03/87598468_p6_master1200.jpg", +# "regular": "https://i.pximg.net/img-master/img/2021/02/07/18/02/03/87598468_p6_master1200.jpg", +# "original": "https://i.pximg.net/img-original/img/2021/02/07/18/02/03/87598468_p6.jpg" +# }, +# "width": 5000, +# "height": 5000 +# }, +# { +# "urls": { +# "thumb_mini": "https://i.pximg.net/c/128x128/img-master/img/2021/02/07/18/02/03/87598468_p7_square1200.jpg", +# "small": "https://i.pximg.net/c/540x540_70/img-master/img/2021/02/07/18/02/03/87598468_p7_master1200.jpg", +# "regular": "https://i.pximg.net/img-master/img/2021/02/07/18/02/03/87598468_p7_master1200.jpg", +# "original": "https://i.pximg.net/img-original/img/2021/02/07/18/02/03/87598468_p7.jpg" +# }, +# "width": 5000, +# "height": 5000 +# }, +# { +# "urls": { +# "thumb_mini": "https://i.pximg.net/c/128x128/img-master/img/2021/02/07/18/02/03/87598468_p8_square1200.jpg", +# "small": "https://i.pximg.net/c/540x540_70/img-master/img/2021/02/07/18/02/03/87598468_p8_master1200.jpg", +# "regular": "https://i.pximg.net/img-master/img/2021/02/07/18/02/03/87598468_p8_master1200.jpg", +# "original": "https://i.pximg.net/img-original/img/2021/02/07/18/02/03/87598468_p8.jpg" +# }, +# "width": 5000, +# "height": 5000 +# }, +# { +# "urls": { +# "thumb_mini": "https://i.pximg.net/c/128x128/img-master/img/2021/02/07/18/02/03/87598468_p9_square1200.jpg", +# "small": "https://i.pximg.net/c/540x540_70/img-master/img/2021/02/07/18/02/03/87598468_p9_master1200.jpg", +# "regular": "https://i.pximg.net/img-master/img/2021/02/07/18/02/03/87598468_p9_master1200.jpg", +# "original": "https://i.pximg.net/img-original/img/2021/02/07/18/02/03/87598468_p9.jpg" +# }, +# "width": 5000, +# "height": 5000 +# }, +# { +# "urls": { +# "thumb_mini": "https://i.pximg.net/c/128x128/img-master/img/2021/02/07/18/02/03/87598468_p10_square1200.jpg", +# "small": "https://i.pximg.net/c/540x540_70/img-master/img/2021/02/07/18/02/03/87598468_p10_master1200.jpg", +# "regular": "https://i.pximg.net/img-master/img/2021/02/07/18/02/03/87598468_p10_master1200.jpg", +# "original": "https://i.pximg.net/img-original/img/2021/02/07/18/02/03/87598468_p10.jpg" +# }, +# "width": 5000, +# "height": 5000 +# }, +# { +# "urls": { +# "thumb_mini": "https://i.pximg.net/c/128x128/img-master/img/2021/02/07/18/02/03/87598468_p11_square1200.jpg", +# "small": "https://i.pximg.net/c/540x540_70/img-master/img/2021/02/07/18/02/03/87598468_p11_master1200.jpg", +# "regular": "https://i.pximg.net/img-master/img/2021/02/07/18/02/03/87598468_p11_master1200.jpg", +# "original": "https://i.pximg.net/img-original/img/2021/02/07/18/02/03/87598468_p11.jpg" +# }, +# "width": 5000, +# "height": 5000 +# }, +# { +# "urls": { +# "thumb_mini": "https://i.pximg.net/c/128x128/img-master/img/2021/02/07/18/02/03/87598468_p12_square1200.jpg", +# "small": "https://i.pximg.net/c/540x540_70/img-master/img/2021/02/07/18/02/03/87598468_p12_master1200.jpg", +# "regular": "https://i.pximg.net/img-master/img/2021/02/07/18/02/03/87598468_p12_master1200.jpg", +# "original": "https://i.pximg.net/img-original/img/2021/02/07/18/02/03/87598468_p12.jpg" +# }, +# "width": 5000, +# "height": 5000 +# }, +# { +# "urls": { +# "thumb_mini": "https://i.pximg.net/c/128x128/img-master/img/2021/02/07/18/02/03/87598468_p13_square1200.jpg", +# "small": "https://i.pximg.net/c/540x540_70/img-master/img/2021/02/07/18/02/03/87598468_p13_master1200.jpg", +# "regular": "https://i.pximg.net/img-master/img/2021/02/07/18/02/03/87598468_p13_master1200.jpg", +# "original": "https://i.pximg.net/img-original/img/2021/02/07/18/02/03/87598468_p13.jpg" +# }, +# "width": 5000, +# "height": 5000 +# }, +# { +# "urls": { +# "thumb_mini": "https://i.pximg.net/c/128x128/img-master/img/2021/02/07/18/02/03/87598468_p14_square1200.jpg", +# "small": "https://i.pximg.net/c/540x540_70/img-master/img/2021/02/07/18/02/03/87598468_p14_master1200.jpg", +# "regular": "https://i.pximg.net/img-master/img/2021/02/07/18/02/03/87598468_p14_master1200.jpg", +# "original": "https://i.pximg.net/img-original/img/2021/02/07/18/02/03/87598468_p14.jpg" +# }, +# "width": 5000, +# "height": 5000 +# }, +# { +# "urls": { +# "thumb_mini": "https://i.pximg.net/c/128x128/img-master/img/2021/02/07/18/02/03/87598468_p15_square1200.jpg", +# "small": "https://i.pximg.net/c/540x540_70/img-master/img/2021/02/07/18/02/03/87598468_p15_master1200.jpg", +# "regular": "https://i.pximg.net/img-master/img/2021/02/07/18/02/03/87598468_p15_master1200.jpg", +# "original": "https://i.pximg.net/img-original/img/2021/02/07/18/02/03/87598468_p15.jpg" +# }, +# "width": 5000, +# "height": 5000 +# } +# ] +# } +# +# https://www.pixiv.net/ajax/illust/74932152/ugoira_meta?lang=en +# +# => { +# "error": false, +# "message": "", +# "body": { +# "src": "https://i.pximg.net/img-zip-ugoira/img/2019/05/27/17/59/33/74932152_ugoira600x600.zip", +# "originalSrc": "https://i.pximg.net/img-zip-ugoira/img/2019/05/27/17/59/33/74932152_ugoira1920x1080.zip", +# "mime_type": "image/jpeg", +# "frames": [ +# { +# "file": "000000.jpg", +# "delay": 60 +# }, +# { +# "file": "000001.jpg", +# "delay": 60 +# }, +# { +# "file": "000002.jpg", +# "delay": 60 +# }, +# { +# "file": "000003.jpg", +# "delay": 60 +# }, +# { +# "file": "000004.jpg", +# "delay": 60 +# }, +# { +# "file": "000005.jpg", +# "delay": 60 +# }, +# { +# "file": "000006.jpg", +# "delay": 60 +# }, +# { +# "file": "000007.jpg", +# "delay": 60 +# } +# ] +# } +# } + +class PixivAjaxClient + USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36" + + attr_reader :phpsessid, :http + + def initialize(phpsessid, http: Danbooru::Http.new) + @phpsessid = phpsessid + @http = http + end + + def illust(illust_id) + get("https://www.pixiv.net/ajax/illust/#{illust_id}").with_indifferent_access + end + + def pages(illust_id) + get("https://www.pixiv.net/ajax/illust/#{illust_id}/pages") + end + + def ugoira_meta(illust_id) + get("https://www.pixiv.net/ajax/illust/#{illust_id}/ugoira_meta").with_indifferent_access + end + + def get(url) + response = client.cache(1.minute).get(url) + + if response.code == 200 + response.parse["body"] + else + DanbooruLogger.info("Pixiv API call failed (url=#{url} status=#{response.code} body=#{response.body})") + {} + end + end + + def client + @client ||= http.headers("User-Agent": USER_AGENT).cookies(PHPSESSID: phpsessid) + end +end diff --git a/app/logical/pixiv_api_client.rb b/app/logical/pixiv_api_client.rb deleted file mode 100644 index 22e6c9955..000000000 --- a/app/logical/pixiv_api_client.rb +++ /dev/null @@ -1,184 +0,0 @@ -class PixivApiClient - extend Memoist - - API_VERSION = "1" - CLIENT_ID = "bYGKuGVw91e0NMfPGp44euvGt59s" - CLIENT_SECRET = "HP3RmkgAmEGro0gn1x9ioawQE8WMfvLXDz3ZqxpK" - CLIENT_HASH_SALT = "28c1fdd170a5204386cb1313c7077b34f83e4aaf4aa829ce78c231e05b0bae2c" - - # Tools to not include in the tags list. We don't tag digital media, so - # including these results in bad translated tags suggestions. - TOOLS_BLACKLIST = %w[ - Photoshop Illustrator Fireworks Flash Painter PaintShopPro pixiv\ Sketch - CLIP\ STUDIO\ PAINT IllustStudio ComicStudio RETAS\ STUDIO SAI PhotoStudio - Pixia NekoPaint PictBear openCanvas ArtRage Expression Inkscape GIMP - CGillust COMICWORKS MS_Paint EDGE AzPainter AzPainter2 AzDrawing - PicturePublisher SketchBookPro Processing 4thPaint GraphicsGale mdiapp - Paintgraphic AfterEffects drawr CLIP\ PAINT\ Lab FireAlpaca Pixelmator - AzDrawing2 MediBang\ Paint Krita ibisPaint Procreate Live2D - Lightwave3D Shade Poser STRATA AnimationMaster XSI CARRARA CINEMA4D Maya - 3dsMax Blender ZBrush Metasequoia Sunny3D Bryce Vue Hexagon\ King SketchUp - VistaPro Sculptris Comi\ Po! modo DAZ\ Studio 3D-Coat - ] - - class Error < StandardError; end - class BadIDError < Error; end - - class WorkResponse - attr_reader :json, :pages, :name, :moniker, :user_id, :page_count, :tags - attr_reader :artist_commentary_title, :artist_commentary_desc - - def initialize(json) - @json = json - @name = json["user"]["name"] - @user_id = json["user"]["id"] - @moniker = json["user"]["account"] - @page_count = json["page_count"].to_i - @artist_commentary_title = json["title"].to_s - @artist_commentary_desc = json["caption"].to_s - @tags = json["tags"].reject {|x| x =~ /^http:/} - @tags += json["tools"] - TOOLS_BLACKLIST - - if json["metadata"] - if json["metadata"]["zip_urls"] - @pages = json["metadata"]["zip_urls"] - elsif page_count > 1 - @pages = json["metadata"]["pages"].map {|x| x["image_urls"]["large"]} - end - end - - if @pages.nil? && json["image_urls"] - @pages = [json["image_urls"]["large"]] - end - end - end - - class NovelResponse - extend Memoist - - attr_reader :json - - def initialize(json) - @json = json - end - - def name - json["user"]["name"] - end - - def user_id - json["user"]["id"] - end - - def moniker - json["user"]["account"] - end - - def page_count - json["page_count"].to_i - end - - def artist_commentary_title - json["title"] - end - - def artist_commentary_desc - json["caption"] - end - - def tags - json["tags"] - end - - def pages - # ex: - # https://i.pximg.net/c/150x150_80/novel-cover-master/img/2017/07/27/23/14/17/8465454_80685d10e6df4d7d53ad347ddc18a36b_master1200.jpg (6096b) - # => - # https://i.pximg.net/novel-cover-original/img/2017/07/27/23/14/17/8465454_80685d10e6df4d7d53ad347ddc18a36b.jpg (532129b) - [find_original(json["image_urls"]["small"])] - end - memoize :pages - - public - - PXIMG = %r!\Ahttps?://i\.pximg\.net/c/\d+x\d+_\d+/novel-cover-master/img/(?\d+/\d+/\d+/\d+/\d+/\d+)/(?\d+_[a-f0-9]+)_master\d+\.(?jpg|jpeg|png|gif)!i - - def find_original(x) - if x =~ PXIMG - return "https://i.pximg.net/novel-cover-original/img/#{$~[:timestamp]}/#{$~[:filename]}.#{$~[:ext]}" - end - - return x - end - end - - def work(illust_id) - params = { image_sizes: "large", include_stats: "true" } - url = "https://public-api.secure.pixiv.net/v#{API_VERSION}/works/#{illust_id.to_i}.json" - response = api_client.cache(1.minute).get(url, params: params) - json = response.parse - - if response.status == 200 - WorkResponse.new(json["response"][0]) - elsif json["status"] == "failure" && json.dig("errors", "system", "message") =~ /対象のイラストは見つかりませんでした。/ - raise BadIDError.new("Pixiv ##{illust_id} not found: work was deleted, made private, or ID is invalid.") - else - raise Error.new("Pixiv API call failed (status=#{response.code} body=#{response.body})") - end - rescue JSON::ParserError - raise Error.new("Pixiv API call failed (status=#{response.code} body=#{response.body})") - end - - def novel(novel_id) - url = "https://public-api.secure.pixiv.net/v#{API_VERSION}/novels/#{novel_id.to_i}.json" - resp = api_client.cache(1.minute).get(url) - json = resp.parse - - if resp.status == 200 - NovelResponse.new(json["response"][0]) - elsif json["status"] == "failure" && json.dig("errors", "system", "message") =~ /対象のイラストは見つかりませんでした。/ - raise Error.new("Pixiv API call failed (status=#{resp.code} body=#{body})") - end - rescue JSON::ParserError - raise Error.new("Pixiv API call failed (status=#{resp.code} body=#{body})") - end - - def access_token - # truncate timestamp to 1-hour resolution so that it doesn't break caching. - client_time = Time.zone.now.utc.change(min: 0).rfc3339 - client_hash = Digest::MD5.hexdigest(client_time + CLIENT_HASH_SALT) - - headers = { - "Referer": "http://www.pixiv.net", - "X-Client-Time": client_time, - "X-Client-Hash": client_hash - } - - params = { - username: Danbooru.config.pixiv_login, - password: Danbooru.config.pixiv_password, - grant_type: "password", - client_id: CLIENT_ID, - client_secret: CLIENT_SECRET - } - - resp = http.headers(headers).cache(1.hour).post("https://oauth.secure.pixiv.net/auth/token", form: params) - return nil unless resp.status == 200 - - resp.parse.dig("response", "access_token") - end - - def api_client - http.headers( - "Referer": "http://www.pixiv.net", - "Content-Type": "application/x-www-form-urlencoded", - "Authorization": "Bearer #{access_token}" - ) - end - - def http - Danbooru::Http.new - end - - memoize :access_token, :api_client, :http -end diff --git a/app/logical/sources/strategies.rb b/app/logical/sources/strategies.rb index ca77191ec..fddc1fdd3 100644 --- a/app/logical/sources/strategies.rb +++ b/app/logical/sources/strategies.rb @@ -2,7 +2,7 @@ module Sources module Strategies def self.all [ - #Strategies::Pixiv, + Strategies::Pixiv, Strategies::Fanbox, Strategies::NicoSeiga, Strategies::Twitter, diff --git a/app/logical/sources/strategies/pixiv.rb b/app/logical/sources/strategies/pixiv.rb index 00d5ff86b..0a670351d 100644 --- a/app/logical/sources/strategies/pixiv.rb +++ b/app/logical/sources/strategies/pixiv.rb @@ -48,27 +48,22 @@ module Sources I12 = %r{(?:\A(?:https?://)?i[0-9]+\.pixiv\.net)} IMG = %r{(?:\A(?:https?://)?img[0-9]*\.pixiv\.net)} PXIMG = %r{(?:\A(?:https?://)?[^.]+\.pximg\.net)} - TOUCH = %r{(?:\A(?:https?://)?touch\.pixiv\.net)} UGOIRA = %r{#{PXIMG}/img-zip-ugoira/img/#{DATE}/(?\d+)_ugoira1920x1080\.zip\z}i ORIG_IMAGE = %r{#{PXIMG}/img-original/img/#{DATE}/(?\d+)_p(?\d+)\.#{EXT}\z}i - STACC_PAGE = %r{\A#{WEB}/stacc/#{MONIKER}/?\z}i - NOVEL_PAGE = %r{(?:\Ahttps?://www\.pixiv\.net/novel/show\.php\?id=(\d+))} def self.enabled? - Danbooru.config.pixiv_login.present? && Danbooru.config.pixiv_password.present? + Danbooru.config.pixiv_phpsessid.present? end def self.to_dtext(text) - if text.nil? - return nil - end + return nil if text.nil? - text = text.gsub(%r{https?://www\.pixiv\.net/member_illust\.php\?mode=medium&illust_id=([0-9]+)}i) do |_match| + text = text.gsub(%r{illust/[0-9]+}i) do |_match| pixiv_id = $1 %(pixiv ##{pixiv_id} "»":[#{Routes.posts_path(tags: "pixiv:#{pixiv_id}")}]) end - text = text.gsub(%r{https?://www\.pixiv\.net/member\.php\?id=([0-9]+)}i) do |_match| + text = text.gsub(%r{user/[0-9]+}i) do |_match| member_id = $1 profile_url = "https://www.pixiv.net/users/#{member_id}" artist_search_url = Routes.artists_path(search: { url_matches: profile_url }) @@ -76,7 +71,6 @@ module Sources %("user/#{member_id}":[#{profile_url}] "»":[#{artist_search_url}]) end - text = text.gsub(/\r\n|\r|\n/, "
") DText.from_html(text) end @@ -95,9 +89,19 @@ module Sources end def image_urls - image_urls_sub - rescue PixivApiClient::BadIDError - [url] + if is_ugoira? + [api_ugoira[:originalSrc]] + elsif manga_page.present? && original_urls.present? + [original_urls[manga_page]] + elsif original_urls.present? + original_urls + else + [url] + end + end + + def original_urls + api_pages.map { |page| page.dig("urls", "original") } end def preview_urls @@ -114,17 +118,8 @@ module Sources end def page_url - if novel_id.present? - return "https://www.pixiv.net/novel/show.php?id=#{novel_id}&mode=cover" - end - - if illust_id.present? - return "https://www.pixiv.net/artworks/#{illust_id}" - end - - url - rescue PixivApiClient::BadIDError - nil + return nil if illust_id.blank? + "https://www.pixiv.net/artworks/#{illust_id}" end def canonical_url @@ -132,15 +127,15 @@ module Sources end def profile_url - [url, referer_url].each do |x| - if x =~ PROFILE - return x - end - end + url = urls.find { |url| url.match?(PROFILE) } - "https://www.pixiv.net/users/#{metadata.user_id}" - rescue PixivApiClient::BadIDError - nil + if url.present? + url + elsif api_illust[:userId].present? + "https://www.pixiv.net/users/#{api_illust[:userId]}" + else + nil + end end def stacc_url @@ -153,9 +148,7 @@ module Sources end def artist_name - metadata.name - rescue PixivApiClient::BadIDError - nil + api_illust[:userName] end def other_names @@ -163,15 +156,11 @@ module Sources end def artist_commentary_title - metadata.artist_commentary_title - rescue PixivApiClient::BadIDError - nil + api_illust[:title] end def artist_commentary_desc - metadata.artist_commentary_desc - rescue PixivApiClient::BadIDError - nil + api_illust[:description] end def headers @@ -179,8 +168,7 @@ module Sources end def normalize_for_source - return if illust_id.blank? - + return nil if illust_id.blank? "https://www.pixiv.net/artworks/#{illust_id}" end @@ -189,11 +177,10 @@ module Sources end def tags - metadata.tags.map do |tag| + api_illust.dig(:tags, :tags).to_a.map do |item| + tag = item[:tag] [tag, "https://www.pixiv.net/search.php?s_mode=s_tag_full&#{{word: tag}.to_param}"] end - rescue PixivApiClient::BadIDError - [] end def normalize_tag(tag) @@ -214,28 +201,12 @@ module Sources illust_id.present? ? "pixiv:#{illust_id}" : "source:#{canonical_url}" end - def image_urls_sub - # there's too much normalization bullshit we have to deal with - # raw urls, so just fetch the canonical url from the api every - # time. - if manga_page.present? - return [metadata.pages[manga_page]] - end - - if metadata.pages.is_a?(Hash) - return [ugoira_zip_url] - end - - metadata.pages + def is_ugoira? + # https://i.pximg.net/img-original/img/2019/05/27/17/59/33/74932152_ugoira0.jpg + url.match?(UGOIRA) || api_illust.dig(:urls, :original)&.match?(/ugoira/) end - # in order to prevent recursive loops, this method should not make any - # api calls and only try to extract the illust_id from the url. therefore, - # even though it makes sense to reference page_url here, it will only look - # at (url, referer_url). def illust_id - return nil if novel_id.present? - parsed_urls.each do |url| # http://www.pixiv.net/member_illust.php?mode=medium&illust_id=18557054 # http://www.pixiv.net/member_illust.php?mode=big&illust_id=18557054 @@ -284,27 +255,22 @@ module Sources nil end - memoize :illust_id - def novel_id - [url, referer_url].each do |x| - if x =~ NOVEL_PAGE - return $1 - end - end - - nil + def api_client + PixivAjaxClient.new(Danbooru.config.pixiv_phpsessid) end - memoize :novel_id - def metadata - if novel_id.present? - return PixivApiClient.new.novel(novel_id) - end - - PixivApiClient.new.work(illust_id) + def api_illust + api_client.illust(illust_id) + end + + def api_pages + api_client.pages(illust_id) + end + + def api_ugoira + api_client.ugoira_meta(illust_id) end - memoize :metadata def moniker # we can sometimes get the moniker from the url @@ -315,44 +281,17 @@ module Sources elsif url =~ %r{#{WEB}/stacc/(#{MONIKER})/?$}i $1 else - metadata.moniker + api_illust[:userAccount] end - rescue PixivApiClient::BadIDError - nil end - memoize :moniker def data - { ugoira_frame_data: ugoira_frame_data } + { ugoira_frame_data: api_ugoira[:frames] } end - def ugoira_zip_url - if metadata.pages.is_a?(Hash) && metadata.pages["ugoira600x600"] - metadata.pages["ugoira600x600"].sub("_ugoira600x600.zip", "_ugoira1920x1080.zip") - end - end - memoize :ugoira_zip_url - - def ugoira_frame_data - metadata.json.dig("metadata", "frames") - rescue PixivApiClient::BadIDError - nil - end - memoize :ugoira_frame_data - def ugoira_content_type - case metadata.json["image_urls"].to_s - when /\.jpg/ - "image/jpeg" - when /\.png/ - "image/png" - when /\.gif/ - "image/gif" - else - raise Sources::Error, "content type not found for (#{url}, #{referer_url})" - end + api_ugoira[:mime_type] end - memoize :ugoira_content_type # Returns the current page number of the manga. This will not # make any api calls and only looks at (url, referer_url). @@ -373,7 +312,8 @@ module Sources nil end - memoize :manga_page + + memoize :illust_id, :api_client, :api_illust, :api_pages, :api_ugoira end end end diff --git a/config/danbooru_default_config.rb b/config/danbooru_default_config.rb index 526891393..9e890c39f 100644 --- a/config/danbooru_default_config.rb +++ b/config/danbooru_default_config.rb @@ -289,11 +289,10 @@ module Danbooru [] end - def pixiv_login - nil - end - - def pixiv_password + # Your Pixiv PHPSESSID cookie. Get this by logging in to Pixiv and using + # the devtools to find the PHPSESSID cookie. This is need for Pixiv upload + # support. + def pixiv_phpsessid nil end diff --git a/test/unit/downloads/pixiv_test.rb b/test/unit/downloads/pixiv_test.rb index 7b2ea08b7..11dcf78c2 100644 --- a/test/unit/downloads/pixiv_test.rb +++ b/test/unit/downloads/pixiv_test.rb @@ -129,11 +129,7 @@ module Downloads @strategy = Sources::Strategies.find("http://www.pixiv.net/member_illust.php?mode=medium&illust_id=62247364") assert_equal(2, @strategy.data[:ugoira_frame_data].size) - if @strategy.data[:ugoira_frame_data][0]["file"] - assert_equal([{"file" => "000000.jpg", "delay" => 125}, {"file" => "000001.jpg", "delay" => 125}], @download.data[:ugoira_frame_data]) - else - assert_equal([{"delay_msec" => 125}, {"delay_msec" => 125}], @strategy.data[:ugoira_frame_data]) - end + assert_equal([{"file" => "000000.jpg", "delay" => 125}, {"file" => "000001.jpg", "delay" => 125}], @strategy.data[:ugoira_frame_data]) end end end diff --git a/test/unit/sources/pixiv_test.rb b/test/unit/sources/pixiv_test.rb index a69106c3d..480fe19f2 100644 --- a/test/unit/sources/pixiv_test.rb +++ b/test/unit/sources/pixiv_test.rb @@ -44,12 +44,10 @@ module Sources end should "capture the frame data" do - assert_equal(2, @site.ugoira_frame_data.size) - if @site.ugoira_frame_data[0]["file"] - assert_equal([{"file" => "000000.jpg", "delay" => 125}, {"file" => "000001.jpg", "delay" => 125}], @site.ugoira_frame_data) - else - assert_equal([{"delay_msec" => 125}, {"delay_msec" => 125}], @site.ugoira_frame_data) - end + ugoira_frame_data = @site.data[:ugoira_frame_data] + + assert_equal(2, ugoira_frame_data.size) + assert_equal([{"file" => "000000.jpg", "delay" => 125}, {"file" => "000001.jpg", "delay" => 125}], ugoira_frame_data) end end @@ -195,7 +193,7 @@ module Sources should "convert illust links and member links to dtext" do get_source("https://www.pixiv.net/member_illust.php?mode=medium&illust_id=63421642") - dtext_desc = %(foo 【pixiv #46337015 "»":[/posts?tags=pixiv%3A46337015]】bar 【pixiv #14901720 "»":[/posts?tags=pixiv%3A14901720]】\n\nbaz【"user/83739":[https://www.pixiv.net/users/83739] "»":[/artists?search%5Burl_matches%5D=https%3A%2F%2Fwww.pixiv.net%2Fusers%2F83739]】) + dtext_desc = %(foo 【[b]pixiv #46337015 "»":[/posts?tags=pixiv%3A46337015][/b]】bar 【[b]pixiv #14901720 "»":[/posts?tags=pixiv%3A14901720][/b]】\n\nbaz【[b]"user/83739":[https://www.pixiv.net/users/83739] "»":[/artists?search%5Burl_matches%5D=https%3A%2F%2Fwww.pixiv.net%2Fusers%2F83739][/b]】) assert_equal(dtext_desc, @site.dtext_artist_commentary_desc) end end