243 lines
5.9 KiB
Go
243 lines
5.9 KiB
Go
|
/*
|
||
|
* This file is part of goboru. (https://git.redxen.eu/caskd/goboru)
|
||
|
* Copyright (c) 2022 Alex-David Denes
|
||
|
*
|
||
|
* goboru is free software: you can redistribute it and/or modify
|
||
|
* it under the terms of the GNU General Public License as published by
|
||
|
* the Free Software Foundation, either version 3 of the License, or
|
||
|
* any later version.
|
||
|
*
|
||
|
* goboru is distributed in the hope that it will be useful,
|
||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
* GNU General Public License for more details.
|
||
|
*
|
||
|
* You should have received a copy of the GNU General Public License
|
||
|
* along with goboru. If not, see <https://www.gnu.org/licenses/>.
|
||
|
*/
|
||
|
|
||
|
package e621
|
||
|
|
||
|
import (
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"log"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
"sort"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
. "git.redxen.eu/caskd/goboru"
|
||
|
)
|
||
|
|
||
|
type (
|
||
|
user_id uint64
|
||
|
post_id uint64
|
||
|
pool_id uint64
|
||
|
file_size uint64
|
||
|
file_struct struct {
|
||
|
Width uint64 `json:"width"`
|
||
|
Height uint64 `json:"height"`
|
||
|
URL string `json:"url"`
|
||
|
}
|
||
|
e621_API struct {
|
||
|
Posts []struct {
|
||
|
ID post_id `json:"id"`
|
||
|
CreatedAt string `json:"created_at"`
|
||
|
UpdatedAt string `json:"updated_at"`
|
||
|
File struct {
|
||
|
file_struct
|
||
|
Size file_size `json:"size"`
|
||
|
Ext string `json:"ext"`
|
||
|
Md5 string `json:"md5"`
|
||
|
} `json:"file"`
|
||
|
Preview file_struct `json:"preview"`
|
||
|
Sample struct {
|
||
|
file_struct
|
||
|
Has bool `json:"has"`
|
||
|
Alternates map[string]struct {
|
||
|
Type string `json:"type"`
|
||
|
Height int `json:"height"`
|
||
|
Width int `json:"width"`
|
||
|
Urls []string `json:"urls"`
|
||
|
} `json:"alternates"`
|
||
|
} `json:"sample"`
|
||
|
Score struct {
|
||
|
Up int64 `json:"up"`
|
||
|
Down int64 `json:"down"`
|
||
|
Total int64 `json:"total"`
|
||
|
} `json:"score"`
|
||
|
Tags struct {
|
||
|
General []string `json:"general"`
|
||
|
Species []string `json:"species"`
|
||
|
Character []string `json:"character"`
|
||
|
Copyright []string `json:"copyright"`
|
||
|
Artist []string `json:"artist"`
|
||
|
Invalid []string `json:"invalid"`
|
||
|
Lore []string `json:"lore"`
|
||
|
Meta []string `json:"meta"`
|
||
|
} `json:"tags"`
|
||
|
LockedTags []string `json:"locked_tags"`
|
||
|
ChangeSeq uint64 `json:"change_seq"`
|
||
|
Flags struct {
|
||
|
Pending bool `json:"pending"`
|
||
|
Flagged bool `json:"flagged"`
|
||
|
NoteLocked bool `json:"note_locked"`
|
||
|
StatusLocked bool `json:"status_locked"`
|
||
|
RatingLocked bool `json:"rating_locked"`
|
||
|
CommentDisabled bool `json:"comment_disabled"`
|
||
|
Deleted bool `json:"deleted"`
|
||
|
} `json:"flags"`
|
||
|
Rating string `json:"rating"`
|
||
|
FavCount uint64 `json:"fav_count"`
|
||
|
Sources []string `json:"sources"`
|
||
|
Pools []pool_id `json:"pools"`
|
||
|
Relationships struct {
|
||
|
ParentID post_id `json:"parent_id"`
|
||
|
HasChildren bool `json:"has_children"`
|
||
|
HasActiveChildren bool `json:"has_active_children"`
|
||
|
Children []post_id `json:"children"`
|
||
|
} `json:"relationships"`
|
||
|
ApproverID user_id `json:"approver_id"`
|
||
|
UploaderID user_id `json:"uploader_id"`
|
||
|
Description string `json:"description"`
|
||
|
CommentCount uint64 `json:"comment_count"`
|
||
|
IsFavorited bool `json:"is_favorited"`
|
||
|
HasNotes bool `json:"has_notes"`
|
||
|
Duration float32 `json:"duration"`
|
||
|
} `json:"posts"`
|
||
|
}
|
||
|
)
|
||
|
|
||
|
type result struct {
|
||
|
media []Media
|
||
|
err error
|
||
|
pid uint
|
||
|
}
|
||
|
|
||
|
func Query(tags Tags, j_max Jobs) (mr []Media, err error) {
|
||
|
res_chan := make(chan result)
|
||
|
var r_arr []result
|
||
|
|
||
|
for pid, rpid, ppid := uint(0), uint(0), uint(0); ; {
|
||
|
go run_job(tags, pid, res_chan)
|
||
|
pid++
|
||
|
|
||
|
if pid < uint(j_max) {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if rpid < pid {
|
||
|
r := <-res_chan
|
||
|
rpid++
|
||
|
r_arr = append(r_arr, r)
|
||
|
}
|
||
|
|
||
|
if ppid < pid {
|
||
|
sort.Slice(r_arr, func(i, j int) bool {
|
||
|
return r_arr[i].pid < r_arr[j].pid
|
||
|
})
|
||
|
|
||
|
if c := r_arr[0]; c.pid == ppid {
|
||
|
ppid++
|
||
|
r_arr = r_arr[1:]
|
||
|
if c.err != nil {
|
||
|
err = c.err
|
||
|
break
|
||
|
}
|
||
|
if len(c.media) == 0 {
|
||
|
break
|
||
|
}
|
||
|
mr = append(mr, c.media...)
|
||
|
log.Print("Added ", len(c.media), "/", len(mr), " elements")
|
||
|
}
|
||
|
} else {
|
||
|
break // Break when no more pages have been fetched
|
||
|
}
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func run_job(tags []string, pid uint, res chan result) {
|
||
|
r := result{pid: pid}
|
||
|
defer func(x *result, c chan result) { c <- *x }(&r, res)
|
||
|
|
||
|
var rc io.ReadCloser
|
||
|
if rc, r.err = fetch(tags, pid); r.err != nil {
|
||
|
return
|
||
|
}
|
||
|
defer rc.Close()
|
||
|
|
||
|
if r.media, r.err = parse(rc); r.err != nil {
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func fetch(tags []string, pid uint) (rc io.ReadCloser, err error) {
|
||
|
client := http.Client{Timeout: 10 * time.Second}
|
||
|
req := &http.Request{
|
||
|
URL: &url.URL{
|
||
|
Scheme: "https",
|
||
|
Host: "e621.net",
|
||
|
Path: "/posts.json",
|
||
|
},
|
||
|
}
|
||
|
query := req.URL.Query()
|
||
|
query.Add("page", strconv.FormatUint(uint64(pid), 10))
|
||
|
query.Add("tags", strings.Join(tags, " "))
|
||
|
req.URL.RawQuery = query.Encode()
|
||
|
req.Header = make(http.Header)
|
||
|
req.Header.Set("user-agent", "gomon/1.0 (https://git.redxen.eu/caskd/gomon)")
|
||
|
|
||
|
resp, err := client.Do(req)
|
||
|
if err != nil {
|
||
|
return
|
||
|
}
|
||
|
rc = resp.Body
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func parse(r io.Reader) (m []Media, err error) {
|
||
|
var api_resp e621_API
|
||
|
|
||
|
d := json.NewDecoder(r)
|
||
|
d.DisallowUnknownFields()
|
||
|
if err = d.Decode(&api_resp); err != nil {
|
||
|
err = fmt.Errorf("JSON parse: %s", err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if len(api_resp.Posts) == 0 {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
for _, v := range api_resp.Posts {
|
||
|
if v.File.URL == "" {
|
||
|
// Skip posts that require higher priviledges to access
|
||
|
// - "Some posts cannot be viewed without logging in based on which tags are applied to them."
|
||
|
// https://e621.net/forum_topics/25717
|
||
|
continue
|
||
|
}
|
||
|
cur := Media{
|
||
|
Source: v.File.URL,
|
||
|
MD5: v.File.Md5,
|
||
|
}
|
||
|
for _, tc := range [][]string{
|
||
|
v.Tags.General,
|
||
|
v.Tags.Species,
|
||
|
v.Tags.Character,
|
||
|
v.Tags.Copyright,
|
||
|
v.Tags.Artist,
|
||
|
v.Tags.Lore,
|
||
|
v.Tags.Meta,
|
||
|
} {
|
||
|
cur.Tags = append(cur.Tags, tc...)
|
||
|
}
|
||
|
m = append(m, cur)
|
||
|
}
|
||
|
return
|
||
|
}
|