mirror of
https://github.com/gabehf/sonarr-anime-importer.git
synced 2026-03-18 11:46:24 -07:00
feat: Anilist support
This commit is contained in:
parent
ec6c1cc0e0
commit
cdd7b67003
8 changed files with 541 additions and 143 deletions
160
main.go
160
main.go
|
|
@ -5,142 +5,67 @@ import (
|
|||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/darenliang/jikan-go"
|
||||
)
|
||||
|
||||
type ResponseItem struct {
|
||||
Title string `json:"title"`
|
||||
MalId int `json:"malId"`
|
||||
TvdbId int `json:"tvdbId"`
|
||||
Title string `json:"title"`
|
||||
TitleEng string `json:"titleEnglish,omitempty"`
|
||||
MalId int `json:"malId,omitempty"`
|
||||
AnilistId int `json:"anilistId,omitempty"`
|
||||
TvdbId int `json:"tvdbId"`
|
||||
}
|
||||
type AnimeEntry struct {
|
||||
TvdbId int `json:"tvdb_id"`
|
||||
MalId interface{} `json:"mal_id"`
|
||||
TvdbId int `json:"tvdb_id"`
|
||||
MalId interface{} `json:"mal_id"`
|
||||
AnilistId int `json:"anilist_id"`
|
||||
}
|
||||
type ConcurrentMap struct {
|
||||
m map[int]int
|
||||
mut sync.RWMutex
|
||||
mal map[int]int
|
||||
anilist map[int]int
|
||||
mut sync.RWMutex
|
||||
}
|
||||
|
||||
func (m *ConcurrentMap) Get(i int) int {
|
||||
func (m *ConcurrentMap) GetByMalId(i int) int {
|
||||
m.mut.RLock()
|
||||
defer m.mut.RUnlock()
|
||||
return m.m[i]
|
||||
return m.mal[i]
|
||||
}
|
||||
func (m *ConcurrentMap) GetByAnilistId(i int) int {
|
||||
m.mut.RLock()
|
||||
defer m.mut.RUnlock()
|
||||
return m.anilist[i]
|
||||
}
|
||||
|
||||
var lastBuiltAnimeIdList time.Time
|
||||
|
||||
func main() {
|
||||
log.Println("sonarr-mal-importer v0.1.1")
|
||||
log.Println("sonarr-anime-importer v0.2.0")
|
||||
log.Println("Building Anime ID Associations...")
|
||||
var malToTvdb = new(ConcurrentMap)
|
||||
buildIdMap(malToTvdb)
|
||||
var idMap = new(ConcurrentMap)
|
||||
buildIdMap(idMap)
|
||||
permaSkipMalStr := os.Getenv("ALWAYS_SKIP_MAL_IDS")
|
||||
permaSkipMalIds := strings.Split(permaSkipMalStr, ",")
|
||||
if permaSkipMalStr != "" {
|
||||
log.Printf("Always skipping: %v\n", permaSkipMalIds)
|
||||
log.Printf("Always skipping MAL IDs: %v\n", permaSkipMalIds)
|
||||
}
|
||||
http.HandleFunc("/anime", handleAnimeSearch(malToTvdb, permaSkipMalIds))
|
||||
permaSkipAnilistStr := os.Getenv("ALWAYS_SKIP_ANILIST_IDS")
|
||||
permaSkipAnilistIds := strings.Split(permaSkipAnilistStr, ",")
|
||||
if permaSkipAnilistStr != "" {
|
||||
log.Printf("Always skipping Anilist IDs: %v\n", permaSkipAnilistIds)
|
||||
}
|
||||
buildIdMapMiddleware := newRebuildStaleIdMapMiddleware(idMap)
|
||||
http.HandleFunc("/v1/mal/anime", loggerMiddleware(buildIdMapMiddleware(handleMalAnimeSearch(idMap, permaSkipMalIds))))
|
||||
http.HandleFunc("/v1/anilist/anime", loggerMiddleware(buildIdMapMiddleware(handleAnilistAnimeSearch(idMap, permaSkipAnilistIds))))
|
||||
log.Println("Listening on :3333")
|
||||
log.Fatal(http.ListenAndServe(":3333", nil))
|
||||
}
|
||||
|
||||
func handleAnimeSearch(malToTvdb *ConcurrentMap, permaSkipMalIds []string) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("%s %s?%s", r.Method, r.URL.Path, r.URL.RawQuery)
|
||||
if time.Since(lastBuiltAnimeIdList) > 24*time.Hour {
|
||||
log.Println("Anime ID association table expired, building new table...")
|
||||
buildIdMap(malToTvdb)
|
||||
}
|
||||
search, err := getAnimeSearch(malToTvdb, permaSkipMalIds, r)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
} else {
|
||||
w.Write([]byte(search))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getAnimeSearch(malToTvdb *ConcurrentMap, permaSkipMalIds []string, r *http.Request) (string, error) {
|
||||
q := r.URL.Query()
|
||||
|
||||
limit, err := strconv.Atoi(q.Get("limit"))
|
||||
if err != nil {
|
||||
limit = 9999 // limit not specified or invalid
|
||||
}
|
||||
|
||||
skipDedup := parseBoolParam(q, "allow_duplicates")
|
||||
|
||||
// for some reason Jikan responds with 400 Bad Request for any limit >25
|
||||
// so instead, we just limit when mapping the data and remove the limit from the Jikan request
|
||||
q.Del("limit")
|
||||
|
||||
hasNextPage := true
|
||||
page := 0
|
||||
resp := []ResponseItem{}
|
||||
count := 0
|
||||
usedIds := make(map[int]bool, 0)
|
||||
for hasNextPage {
|
||||
page++
|
||||
q.Set("page", strconv.Itoa(page))
|
||||
result, err := jikan.GetAnimeSearch(q)
|
||||
if err != nil {
|
||||
log.Println("Error sending request to Jikan: ", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// map the data
|
||||
for _, item := range result.Data {
|
||||
if malToTvdb.Get(item.MalId) == 0 {
|
||||
log.Printf("MyAnimeList ID %d (%s a.k.a. %s) has no associated TVDB ID, skipping...\n", item.MalId, item.Title, item.TitleEnglish)
|
||||
continue
|
||||
}
|
||||
if usedIds[item.MalId] && !skipDedup {
|
||||
log.Printf("MyAnimeList ID %d (%s a.k.a. %s) is a duplicate, skipping...\n", item.MalId, item.Title, item.TitleEnglish)
|
||||
continue
|
||||
}
|
||||
if slices.Contains(permaSkipMalIds, strconv.Itoa(item.MalId)) {
|
||||
log.Printf("MyAnimeList ID %d (%s a.k.a. %s) is set to always skip, skipping...\n", item.MalId, item.Title, item.TitleEnglish)
|
||||
continue
|
||||
}
|
||||
count++
|
||||
if count > limit {
|
||||
break
|
||||
}
|
||||
resp = append(resp,
|
||||
ResponseItem{
|
||||
item.Title,
|
||||
item.MalId,
|
||||
malToTvdb.Get(item.MalId),
|
||||
})
|
||||
usedIds[item.MalId] = true
|
||||
}
|
||||
hasNextPage = result.Pagination.HasNextPage
|
||||
if count > limit {
|
||||
break
|
||||
}
|
||||
if hasNextPage {
|
||||
time.Sleep(500 * time.Millisecond) // sleep between requests for new page to try and avoid rate limits
|
||||
}
|
||||
}
|
||||
|
||||
respJson, err := json.MarshalIndent(resp, "", " ")
|
||||
if err != nil {
|
||||
log.Println("Error marshalling response: ", err)
|
||||
return "", err
|
||||
}
|
||||
return string(respJson), nil
|
||||
}
|
||||
|
||||
func buildIdMap(idMap *ConcurrentMap) {
|
||||
// build/re-build the mal -> tvdb association table
|
||||
idMap.mut.Lock()
|
||||
|
|
@ -150,6 +75,7 @@ func buildIdMap(idMap *ConcurrentMap) {
|
|||
if err != nil {
|
||||
log.Fatal("Error fetching anime_ids.json: ", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
idListBytes, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Fatal("Error reading anime_ids.json: ", err)
|
||||
|
|
@ -160,7 +86,8 @@ func buildIdMap(idMap *ConcurrentMap) {
|
|||
if err != nil {
|
||||
log.Fatal("Error unmarshalling anime_ids.json: ", err)
|
||||
}
|
||||
idMap.m = make(map[int]int, 0)
|
||||
idMap.mal = make(map[int]int, 0)
|
||||
idMap.anilist = make(map[int]int, 0)
|
||||
for _, entry := range animeMap {
|
||||
if entry.MalId == nil {
|
||||
continue
|
||||
|
|
@ -180,23 +107,12 @@ func buildIdMap(idMap *ConcurrentMap) {
|
|||
malIdList = append(malIdList, int(entry.MalId.(float64)))
|
||||
}
|
||||
for _, val := range malIdList {
|
||||
idMap.m[val] = entry.TvdbId
|
||||
idMap.mal[val] = entry.TvdbId
|
||||
}
|
||||
if entry.AnilistId == 0 {
|
||||
continue
|
||||
}
|
||||
idMap.anilist[entry.AnilistId] = entry.TvdbId
|
||||
}
|
||||
lastBuiltAnimeIdList = time.Now()
|
||||
}
|
||||
|
||||
// parses the boolean param "name" from url.Values "values"
|
||||
func parseBoolParam(values url.Values, name string) bool {
|
||||
param := values.Get(name)
|
||||
|
||||
if param != "" {
|
||||
val, err := strconv.ParseBool(param)
|
||||
if err == nil {
|
||||
return val
|
||||
}
|
||||
} else if _, exists := values[name]; exists {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue