177 lines
4.8 KiB
Go
177 lines
4.8 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
)
|
|
|
|
type CliFlags struct {
|
|
outputFileName string
|
|
manifestFileName string
|
|
userAgent string
|
|
verbose bool
|
|
}
|
|
|
|
type PostId uint64
|
|
|
|
const (
|
|
ArtworkUrlTemplate = "https://www.pixiv.net/en/artworks/%d"
|
|
ManifestUrlTemplate = "https://www.pixiv.net/ajax/illust/%d/ugoira_meta"
|
|
)
|
|
|
|
func doPixivRequest(client *http.Client, url, referer, userAgent string) io.ReadCloser {
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
die("couldn't create request: %s", err)
|
|
}
|
|
|
|
req.Header.Set("Authority", "www.pixiv.net")
|
|
req.Header.Set("Sec-Fetch-Dest", "empty")
|
|
req.Header.Set("Sec-Fetch-Mode", "cors")
|
|
req.Header.Set("Sec-Fetch-Site", "same-origin")
|
|
req.Header.Set("Referer", referer)
|
|
if len(userAgent) > 0 {
|
|
req.Header.Set("User-Agent", userAgent)
|
|
}
|
|
|
|
response, err := client.Do(req)
|
|
if err != nil {
|
|
die("GET request failed: %s", err)
|
|
}
|
|
if response.StatusCode != http.StatusOK {
|
|
die("Got non-200 status code: %d", response.StatusCode)
|
|
}
|
|
return response.Body
|
|
}
|
|
|
|
func getUgoira(postId PostId, flags CliFlags) {
|
|
client := &http.Client{}
|
|
artworkUrl := fmt.Sprintf(ArtworkUrlTemplate, postId)
|
|
manifestUrl := fmt.Sprintf(ManifestUrlTemplate, postId)
|
|
|
|
say(flags.verbose, "Fetching and decoding manifest")
|
|
manifestData := doPixivRequest(client, manifestUrl, artworkUrl, flags.userAgent)
|
|
defer manifestData.Close()
|
|
// Read the entire JSON in memory, since we need to dump it to a file later
|
|
manifestJsonData, err := io.ReadAll(manifestData)
|
|
if err != nil {
|
|
die("error while fetching manifest: %s", err)
|
|
}
|
|
// Now decode the JSON...
|
|
manifest := decodeUgoiraManifest(manifestJsonData)
|
|
if len(manifest.Body.OriginalSrc) == 0 {
|
|
die("Ugoira url is empty")
|
|
}
|
|
say(flags.verbose, "Ugoira file URL: %s", manifest.Body.OriginalSrc)
|
|
|
|
// Download ugoira data
|
|
var ugoiraFileName string
|
|
if len(flags.outputFileName) > 0 {
|
|
ugoiraFileName = flags.outputFileName
|
|
} else {
|
|
ugoiraFileName = fmt.Sprintf("./%d_ugoira.zip", postId)
|
|
}
|
|
say(flags.verbose, "Downloading ugoira (saving to: %s)", ugoiraFileName)
|
|
|
|
ugoiraData := doPixivRequest(client, manifest.Body.OriginalSrc, artworkUrl, flags.userAgent)
|
|
ugoiraFile, err := os.Create(ugoiraFileName)
|
|
if err != nil {
|
|
die("Could not create ugoira file: %s", err)
|
|
}
|
|
defer ugoiraFile.Close()
|
|
if _, err := io.Copy(ugoiraFile, ugoiraData); err != nil {
|
|
die("Ugoira download failed: %s", err)
|
|
}
|
|
|
|
// Dump manifest JSON
|
|
var ugoiraManifestFileName string
|
|
if len(flags.manifestFileName) > 0 {
|
|
ugoiraManifestFileName = flags.manifestFileName
|
|
} else {
|
|
ugoiraManifestFileName = fmt.Sprintf("./%d_ugoira.json", postId)
|
|
}
|
|
say(flags.verbose, "Dumping manifest (to: %s)", ugoiraManifestFileName)
|
|
|
|
ugoiraManifestFile, err := os.Create(ugoiraManifestFileName)
|
|
if err != nil {
|
|
die("Could not create ugoira manifest file: %s", err)
|
|
}
|
|
defer ugoiraManifestFile.Close()
|
|
if _, err := ugoiraManifestFile.Write(manifestJsonData); err != nil {
|
|
die("Ugoira manifest dump failed: %s", err)
|
|
}
|
|
|
|
say(flags.verbose, "Successfully downloaded post %d", postId)
|
|
}
|
|
|
|
func die(format string, args ...any) {
|
|
fmt.Fprintf(os.Stderr, os.Args[0]+": "+format+"\n", args...)
|
|
os.Exit(1)
|
|
}
|
|
|
|
func say(enabled bool, format string, args ...any) {
|
|
if !enabled {
|
|
return
|
|
}
|
|
fmt.Printf(format+"\n", args...)
|
|
}
|
|
|
|
func downloadUgoiraCmd(args []string, flags CliFlags) {
|
|
if len(args) == 0 {
|
|
die("expected post ID")
|
|
}
|
|
postIdStr := args[0]
|
|
postId, err := strconv.ParseUint(postIdStr, 10, 0)
|
|
if err != nil {
|
|
die("Invalid post ID (has to be numerical): '%s'", postIdStr)
|
|
}
|
|
say(flags.verbose, "Working on post %d", postId)
|
|
getUgoira(PostId(postId), flags)
|
|
}
|
|
|
|
func ugoira2videoCmd(args []string, flags CliFlags) {
|
|
if len(args) < 2 {
|
|
die("expected ugoira zip file and manifest JSON")
|
|
}
|
|
_, manifestFileName := args[0], args[1]
|
|
mfp, err := os.Open(manifestFileName)
|
|
if err != nil {
|
|
die("Could not open manifest file: %s", err)
|
|
}
|
|
defer mfp.Close()
|
|
manifestData, err := io.ReadAll(mfp)
|
|
if err != nil {
|
|
die("Could not read manifest: %s", err)
|
|
}
|
|
ugoira2video(decodeUgoiraManifest(manifestData))
|
|
}
|
|
|
|
func main() {
|
|
var flags CliFlags
|
|
downloadCmd := flag.NewFlagSet("download", flag.ExitOnError)
|
|
u2vCmd := flag.NewFlagSet("u2v", flag.ExitOnError)
|
|
|
|
downloadCmd.StringVar(&flags.outputFileName, "o", "", "Ugoira output file name")
|
|
downloadCmd.StringVar(&flags.manifestFileName, "M", "", "Ugoira manifest output file name")
|
|
downloadCmd.StringVar(&flags.userAgent, "A", "", "User agent to use when doing GET requests")
|
|
downloadCmd.BoolVar(&flags.verbose, "v", false, "Enable verbosity")
|
|
|
|
if len(os.Args) < 2 {
|
|
die("expected command")
|
|
}
|
|
|
|
switch os.Args[1] {
|
|
case "download", "down", "d":
|
|
downloadCmd.Parse(os.Args[2:])
|
|
downloadUgoiraCmd(downloadCmd.Args(), flags)
|
|
case "u2v":
|
|
u2vCmd.Parse(os.Args[2:])
|
|
ugoira2videoCmd(u2vCmd.Args(), flags)
|
|
default:
|
|
die("invalid command '%s'", os.Args[1])
|
|
}
|
|
}
|