├── .gitignore ├── Dockerfile ├── README.md ├── TODO.md ├── config.yml.example ├── go.mod ├── go.sum ├── main.go └── modules ├── config ├── main.go └── structs.go ├── debrid └── main.go ├── overseerr ├── anime │ ├── handler.go │ └── main.go ├── movies │ ├── handler.go │ ├── main.go │ └── structs.go └── tv │ ├── handler.go │ ├── main.go │ └── structs.go ├── plex └── main.go ├── structs └── main.go └── torrentio ├── anime └── main.go ├── main.go ├── movies └── main.go └── tv └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | config.yml 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21.3-alpine AS builder 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | RUN go mod tidy 8 | 9 | RUN go build 10 | 11 | CMD ["/app/pod-link"] 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pod-link 2 | Narrowed down alternative to plex debrid. Specifically combining the power of Overseerr, Torrentio and Real debrid. 3 | ## Build 4 | `go build main.go` 5 | 6 | # Configuration 7 | ### pod-link 8 | you can configure `pod-link`'s settings: 9 | ```yml 10 | settings: 11 | pod: 12 | port: 42069 13 | authorization: "Overseerr notification webhook Authorization Header" 14 | ``` 15 | 16 | ### Real Debrid 17 | fill in your [RD token](https://real-debrid.com/apitoken): 18 | ```yml 19 | settings: 20 | real_debrid: 21 | token: "TOKEN" 22 | ``` 23 | 24 | ### Overseerr 25 | ```yml 26 | settings: 27 | overseerr: 28 | host: "http://localhost:5055" 29 | token: "TOKEN:" 30 | ``` 31 | 32 | ### Plex 33 | Get your [plex token](https://github.com/SushyDev/plex-oauth) or [here](https://plex.tv/devices.xml) and get your libary id's 34 | ```yml 35 | settings: 36 | plex: 37 | host: "http://localhost:32400" 38 | token: "TOKEN" 39 | tv_id: 1 40 | movie_id: 2 41 | ``` 42 | 43 | ### Torrentio 44 | Get the torrentio filter options from [here](https://torrentio.strem.fun/configure) 45 | Make sure to only put the filter options and not the entire url. 46 | **Don't** configure Real Debrid (or any other debrid) in Torrentio, `pod-link` will do that for you! 47 | ```yml 48 | settings: 49 | torrentio: 50 | shows: 51 | filter_uri: "sort=qualitysize|qualityfilter=other,scr,cam,unknown" 52 | movies: 53 | filter_uri: "sort=qualitysize|qualityfilter=other,scr,cam,unknown" 54 | ``` 55 | 56 | ### Shows 57 | Here you can configure the regex to determine what `pod-link` should consider a complete season or just a single episode 58 | Defaults supplied in the example should suffice for most usecases 59 | ```yml 60 | shows: 61 | seasons: 62 | - "(?i)[. ]s\\d+[. ]" 63 | - "(?i)[. ]season \\d+[. ]" 64 | episodes: 65 | - "(?i)[. ]e\\d+[. ]" 66 | - "(?i)[. ]episode \\d+[. ]" 67 | ``` 68 | 69 | ### Movies 70 | Here you can configure details about what results should be used for movies. For now the only thing you can configure is the max file count that a movie can have, you can use this to prevent `pod-link` from picking up results which ship lots of m2ts containers 71 | ```yml 72 | movies: 73 | max_files: 10 74 | ``` 75 | 76 | ### Versions 77 | Here you can configure all the versions of a movie/season/episode to download. 78 | You can configure them per media type or for all media types. 79 | Media types: 80 | - all 81 | - movies 82 | - shows 83 | - anime (not implemented yet) 84 | 85 | As you can imagine all the versions in "all" will apply to all media types. 86 | Within each media type you can configure the version, each version must have a unique name. 87 | If you name a version "all" its include and exclude will be appended to all other versions for that media type. 88 | A version must have a name and can have either or both a list of include and exclude regex strings. 89 | Regex is handled by golang's default regex implementation so any limitations there will apply here. 90 | 91 | ### Example config 92 | Open the config.yml in the repo files 93 | 94 | To make `pod-link` actually do something you must set the notification webhook in overseerr to the url of the project (by default `localhost:8080/webhook`) 95 | 96 | ## Credits 97 | [Plex Debrid](https://github.com/itsToggle/plex_debrid/) A lot of the inspiration 98 | 99 | [Torrentio](https://github.com/TheBeastLT/torrentio-scraper) The source for all media 100 | 101 | [Overseerr](https://github.com/sct/overseerr) 102 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - File limit (per version?) 2 | - Cron for new episodes and missing content 3 | -------------------------------------------------------------------------------- /config.yml.example: -------------------------------------------------------------------------------- 1 | settings: 2 | real_debrid: 3 | token: 4 | 5 | overseerr: 6 | host: 7 | token: 8 | 9 | plex: 10 | host: 11 | token: 12 | tv_id: 13 | movie_id: 14 | 15 | torrentio: 16 | filter_uri: "sort=qualitysize|qualityfilter=other,scr,cam,unknown" 17 | 18 | shows: 19 | seasons: 20 | - "(?i)[. ]s\\d+[. ]" 21 | - "(?i)[. ]season \\d+[. ]" 22 | episodes: 23 | - "(?i)[. ]e\\d+[. ]" 24 | - "(?i)[. ]episode \\d+[. ]" 25 | 26 | movies: 27 | max_files: 45 28 | 29 | versions: 30 | all: 31 | - name: "4K HDR" 32 | include: 33 | - "(?i)2160p.*hdr|hdr.*2160p|4k.*hdr|hdr.*4k" 34 | 35 | - name: "4K" 36 | include: 37 | - "(?i)2160p|4k" 38 | exlude: 39 | - "(?i)hdr" 40 | 41 | - name: "1080P HDR" 42 | include: 43 | - "(?i)1080p.*hdr|hdr.*1080p" 44 | 45 | - name: "1080P" 46 | include: 47 | - "(?i)1080p" 48 | exlude: 49 | - "(?i)hdr" 50 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module pod-link 2 | 3 | go 1.20 4 | 5 | require gopkg.in/yaml.v3 v3.0.1 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 2 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 3 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 4 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 5 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | "pod-link/modules/config" 10 | overseerr_movies "pod-link/modules/overseerr/movies" 11 | overseerr_tv "pod-link/modules/overseerr/tv" 12 | "pod-link/modules/structs" 13 | ) 14 | 15 | type RequestData struct { 16 | NotificationType string `json:"notification_type"` 17 | } 18 | 19 | func handleNotification(notificationType string, body []byte) error { 20 | switch notificationType { 21 | case "MEDIA_AUTO_APPROVED": 22 | var mediaAutoApprovedNotification structs.MediaAutoApprovedNotification 23 | err := json.Unmarshal(body, &mediaAutoApprovedNotification) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | handleMediaAutoApprovedNotification(mediaAutoApprovedNotification) 29 | default: 30 | fmt.Println("Unknown notification type") 31 | } 32 | 33 | return nil 34 | } 35 | 36 | func handleMediaAutoApprovedNotification(notification structs.MediaAutoApprovedNotification) { 37 | switch notification.Media.MediaType { 38 | case "movie": 39 | overseerr_movies.Request(notification) 40 | case "tv": 41 | overseerr_tv.Request(notification) 42 | } 43 | } 44 | 45 | func main() { 46 | settings := config.GetSettings() 47 | port := settings.Pod.Port 48 | if port == "" { 49 | port = "8080" 50 | } 51 | 52 | http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) { 53 | if r.Method != http.MethodPost { 54 | fmt.Println("Only POST requests are allowed") 55 | return 56 | } 57 | 58 | if r.Header.Get("Authorization") != settings.Pod.Authorization { 59 | fmt.Println("Unauthorized") 60 | return 61 | } 62 | 63 | body, err := io.ReadAll(r.Body) 64 | if err != nil { 65 | fmt.Println(err) 66 | return 67 | } 68 | 69 | var requestData RequestData 70 | err = json.Unmarshal(body, &requestData) 71 | if err != nil { 72 | fmt.Println(err) 73 | return 74 | } 75 | 76 | err = handleNotification(requestData.NotificationType, body) 77 | if err != nil { 78 | fmt.Println(err) 79 | return 80 | } 81 | 82 | fmt.Println("Finished!") 83 | }) 84 | 85 | log.Println("listening on", port) 86 | log.Fatal(http.ListenAndServe(":"+port, nil)) 87 | } 88 | -------------------------------------------------------------------------------- /modules/config/main.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "gopkg.in/yaml.v3" 6 | "io" 7 | "os" 8 | ) 9 | 10 | func GetConfig() Config { 11 | configFile, err := os.Open("config.yml") 12 | if err != nil { 13 | fmt.Printf("Error opening config file: %v\n", err) 14 | panic(err) 15 | } 16 | 17 | configFileBytes, err := io.ReadAll(configFile) 18 | if err != nil { 19 | fmt.Printf("Error reading config file: %v\n", err) 20 | panic(err) 21 | } 22 | 23 | var config Config 24 | err = yaml.Unmarshal(configFileBytes, &config) 25 | if err != nil { 26 | fmt.Printf("Error unmarshalling config file: %v\n", err) 27 | panic(err) 28 | } 29 | 30 | return config 31 | } 32 | 33 | func GetSettings() Settings { 34 | return GetConfig().Settings 35 | } 36 | 37 | func GetVersions(mediaType string) []Version { 38 | config := GetConfig() 39 | 40 | versions := config.Versions.All 41 | 42 | switch mediaType { 43 | case "movies": 44 | versions = append(versions, config.Versions.Movies...) 45 | case "shows": 46 | versions = append(versions, config.Versions.Shows...) 47 | } 48 | 49 | for i, iVersion := range versions { 50 | if iVersion.Name != "all" { 51 | continue 52 | } 53 | 54 | for j, jVersion := range versions { 55 | if jVersion.Name == "all" { 56 | continue 57 | } 58 | 59 | versions[j].Include = append(versions[j].Include, iVersion.Include...) 60 | versions[j].Exclude = append(versions[j].Exclude, iVersion.Exclude...) 61 | } 62 | 63 | if i == len(versions)-1 { 64 | versions = versions[:i] 65 | break 66 | } 67 | 68 | versions = append(versions[:i], versions[i+1:]...) 69 | } 70 | 71 | return versions 72 | } 73 | -------------------------------------------------------------------------------- /modules/config/structs.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Pod struct { 4 | Port string `yaml:"port"` 5 | Authorization string `yaml:"authorization"` 6 | } 7 | 8 | type RealDebrid struct { 9 | Token string `yaml:"token"` 10 | } 11 | 12 | type Overseerr struct { 13 | Host string `yaml:"host"` 14 | Token string `yaml:"token"` 15 | } 16 | 17 | type Plex struct { 18 | Host string `yaml:"host"` 19 | Token string `yaml:"token"` 20 | TvId string `yaml:"tv_id"` 21 | MovieId string `yaml:"movie_id"` 22 | } 23 | 24 | type Torrentio struct { 25 | Shows struct { 26 | FilterURI string `yaml:"filter_uri"` 27 | } `yaml:"shows"` 28 | Movies struct { 29 | FilterURI string `yaml:"filter_uri"` 30 | } `yaml:"movies"` 31 | } 32 | 33 | type Settings struct { 34 | Pod Pod `yaml:"pod"` 35 | RealDebrid RealDebrid `yaml:"real_debrid"` 36 | Overseerr Overseerr `yaml:"overseerr"` 37 | Plex Plex `yaml:"plex"` 38 | Torrentio Torrentio `yaml:"torrentio"` 39 | } 40 | 41 | type Version struct { 42 | Name string `yaml:"name"` 43 | Include []string `yaml:"include"` 44 | Exclude []string `yaml:"exclude"` 45 | } 46 | 47 | type Versions struct { 48 | All []Version `yaml:"all"` 49 | Movies []Version `yaml:"movies"` 50 | Shows []Version `yaml:"shows"` 51 | } 52 | 53 | type Shows struct { 54 | Seasons []string `yaml:"seasons"` 55 | Episodes []string `yaml:"episodes"` 56 | } 57 | 58 | type Movies struct { 59 | MaxFiles int `yaml:"max_files"` 60 | } 61 | 62 | type Config struct { 63 | Settings Settings `yaml:"settings"` 64 | Shows Shows `yaml:"shows"` 65 | Movies Movies `yaml:"movies"` 66 | Versions Versions `yaml:"versions"` 67 | } 68 | 69 | -------------------------------------------------------------------------------- /modules/debrid/main.go: -------------------------------------------------------------------------------- 1 | package debrid 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "pod-link/modules/config" 10 | ) 11 | 12 | type AddMagnetResponse struct { 13 | Id string `json:"id"` 14 | Uri string `json:"uri"` 15 | } 16 | 17 | func AddMagnet(magnet string, files string) error { 18 | input := url.Values{} 19 | input.Set("magnet", magnet) 20 | 21 | requestBody := input.Encode() 22 | req, err := http.NewRequest("POST", "https://api.real-debrid.com/rest/1.0/torrents/addMagnet", bytes.NewBufferString(requestBody)) 23 | if err != nil { 24 | fmt.Println("Failed to create request") 25 | return err 26 | } 27 | 28 | settings := config.GetSettings() 29 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", settings.RealDebrid.Token)) 30 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 31 | 32 | client := &http.Client{} 33 | 34 | response, err := client.Do(req) 35 | if err != nil { 36 | fmt.Println("Failed to send request") 37 | return err 38 | } 39 | 40 | defer response.Body.Close() 41 | 42 | var data AddMagnetResponse 43 | err = json.NewDecoder(response.Body).Decode(&data) 44 | if err != nil { 45 | fmt.Println("Failed to decode response") 46 | return err 47 | } 48 | 49 | switch response.StatusCode { 50 | case 201: 51 | return selectFiles(data.Id, files) 52 | case 400: 53 | return fmt.Errorf("Bad Request (see error message)") 54 | case 401: 55 | return fmt.Errorf("Bad token (expired, invalid)") 56 | case 403: 57 | return fmt.Errorf("Permission denied (account locked, not premium) or Infringing torrent") 58 | case 503: 59 | return fmt.Errorf("Service unavailable (see error message)") 60 | default: 61 | return fmt.Errorf("Unknown error") 62 | } 63 | } 64 | 65 | func deleteFile(id string) error { 66 | req, err := http.NewRequest("DELETE", fmt.Sprintf("https://api.real-debrid.com/rest/1.0/torrents/delete/%s", id), nil) 67 | if err != nil { 68 | fmt.Println("Failed to create request") 69 | return err 70 | } 71 | 72 | settings := config.GetSettings() 73 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", settings.RealDebrid.Token)) 74 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 75 | 76 | client := &http.Client{} 77 | 78 | response, err := client.Do(req) 79 | if err != nil { 80 | fmt.Println("Failed to send request") 81 | return err 82 | } 83 | 84 | defer response.Body.Close() 85 | 86 | switch response.StatusCode { 87 | case 204: 88 | return nil 89 | case 401: 90 | return fmt.Errorf("Bad token (expired, invalid)") 91 | case 403: 92 | return fmt.Errorf("Permission denied (account locked, not premium)") 93 | case 404: 94 | return fmt.Errorf("Unknown ressource (invalid id)") 95 | default: 96 | return fmt.Errorf("Unknown error") 97 | } 98 | } 99 | 100 | func selectFiles(id string, files string) error { 101 | input := url.Values{} 102 | input.Set("files", files) 103 | 104 | requestBody := input.Encode() 105 | req, err := http.NewRequest("POST", fmt.Sprintf("https://api.real-debrid.com/rest/1.0/torrents/selectFiles/%s", id), bytes.NewBufferString(requestBody)) 106 | if err != nil { 107 | fmt.Println("Failed to create request") 108 | return err 109 | } 110 | 111 | settings := config.GetSettings() 112 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", settings.RealDebrid.Token)) 113 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 114 | 115 | client := &http.Client{} 116 | 117 | response, err := client.Do(req) 118 | if err != nil { 119 | fmt.Println("Failed to send request") 120 | return err 121 | } 122 | 123 | defer response.Body.Close() 124 | 125 | switch response.StatusCode { 126 | case 202: 127 | return fmt.Errorf("Action already done") 128 | case 204: 129 | return nil 130 | case 400: 131 | return fmt.Errorf("Bad Request (see error message)") 132 | case 401: 133 | return fmt.Errorf("Bad token (expired, invalid)") 134 | case 403: 135 | return fmt.Errorf("Permission denied (account locked, not premium)") 136 | case 404: 137 | err := deleteFile(id) 138 | if err != nil { 139 | return err 140 | } 141 | 142 | return fmt.Errorf("Wrong parameter (invalid file id(s)) / Unknown ressource (invalid id)") 143 | default: 144 | return fmt.Errorf("Unknown error") 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /modules/overseerr/anime/handler.go: -------------------------------------------------------------------------------- 1 | package anime 2 | 3 | import ( 4 | "fmt" 5 | "pod-link/modules/structs" 6 | ) 7 | 8 | func Request(notification structs.MediaAutoApprovedNotification) { 9 | details := GetDetails(notification.Media.TmdbId) 10 | fmt.Println("Got request for", details.Name) 11 | 12 | // TODO: Check kitsu if format is (name-season, episode) or (name, (all episodes before + episode)) 13 | // if isAnime(details.Keywords) { 14 | // for _, season := range requestedSeasons { 15 | // if season == 0 { 16 | // continue 17 | // } 18 | // 19 | // kitsuName := strings.ReplaceAll(details.Name, " ", "-") 20 | // kitsuName = strings.ToLower(kitsuName) 21 | // 22 | // if season > 1 { 23 | // kitsuName = fmt.Sprintf("%s-%v", kitsuName, season) 24 | // } 25 | // 26 | // kitsuDetails := kitsu.GetDetails(kitsuName) 27 | // 28 | // for episode := 1; episode <= kitsuDetails.Attributes.EpisodeCount; episode++ { 29 | // results := torrentio_anime.GetList(kitsuDetails.ID, episode) 30 | // 31 | // 32 | // } 33 | // } 34 | // } 35 | } 36 | -------------------------------------------------------------------------------- /modules/overseerr/anime/main.go: -------------------------------------------------------------------------------- 1 | package anime 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | type KitsuData struct { 10 | ID string `json:"id"` 11 | Name string `json:"name"` // Placeholder 12 | Attributes struct { 13 | EpisodeCount int `json:"episodeCount"` 14 | } `json:"attributes"` 15 | } 16 | 17 | type KitsuResponse struct { 18 | Data []KitsuData `json:"data"` 19 | } 20 | 21 | func GetDetails(name string) KitsuData { 22 | url := fmt.Sprintf("https://kitsu.io/api/edge/anime?filter[text]=%s", name) 23 | 24 | req, err := http.NewRequest("GET", url, nil) 25 | if err != nil { 26 | fmt.Println(err) 27 | fmt.Println("Failed to create request") 28 | } 29 | 30 | client := &http.Client{} 31 | 32 | response, err := client.Do(req) 33 | if err != nil { 34 | fmt.Println(err) 35 | fmt.Println("Failed to send request") 36 | } 37 | 38 | defer response.Body.Close() 39 | 40 | var data KitsuResponse 41 | err = json.NewDecoder(response.Body).Decode(&data) 42 | if err != nil { 43 | fmt.Println(err) 44 | fmt.Println("Failed to decode response") 45 | } 46 | 47 | fmt.Println("Found:", data.Data[0].ID) 48 | 49 | return data.Data[0] 50 | } 51 | -------------------------------------------------------------------------------- /modules/overseerr/movies/handler.go: -------------------------------------------------------------------------------- 1 | package movies 2 | 3 | import ( 4 | "fmt" 5 | "pod-link/modules/config" 6 | "pod-link/modules/debrid" 7 | "pod-link/modules/plex" 8 | "pod-link/modules/structs" 9 | "pod-link/modules/torrentio" 10 | torrentio_movies "pod-link/modules/torrentio/movies" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | func FilterProperties(results []torrentio.Stream) []torrentio.Stream { 16 | var filtered []torrentio.Stream 17 | 18 | config := config.GetConfig() 19 | 20 | for _, result := range results { 21 | properties, err := torrentio.GetPropertiesFromStream(result) 22 | if err != nil { 23 | fmt.Println("Failed to get properties. Skipping.") 24 | fmt.Println(err) 25 | continue 26 | } 27 | 28 | if properties.Files == "all" { 29 | filtered = append(filtered, result) 30 | continue 31 | } 32 | 33 | fileCount := len(strings.Split(properties.Files, ",")) 34 | maxFiles := config.Movies.MaxFiles 35 | 36 | if fileCount <= maxFiles { 37 | filtered = append(filtered, result) 38 | } 39 | } 40 | 41 | return filtered 42 | } 43 | 44 | func Request(notification structs.MediaAutoApprovedNotification) { 45 | details, err := GetDetails(notification.Media.TmdbId) 46 | if err != nil { 47 | fmt.Println("Failed to get details") 48 | fmt.Println(err) 49 | return 50 | } 51 | 52 | fmt.Println("Got request for", details.Title) 53 | 54 | results, err := torrentio_movies.GetList(details.ExternalIds.ImdbId) 55 | if err != nil { 56 | fmt.Println("Failed to get results") 57 | fmt.Println(err) 58 | return 59 | } 60 | 61 | results = torrentio.FilterVersions(results, "movies") 62 | results = FilterProperties(results) 63 | 64 | if len(results) == 0 { 65 | fmt.Println("No results found") 66 | return 67 | } 68 | 69 | for _, result := range results { 70 | properties, err := torrentio.GetPropertiesFromStream(result) 71 | if err != nil { 72 | fmt.Println("Failed to get properties. Skipping") 73 | fmt.Println(err) 74 | continue 75 | } 76 | 77 | fmt.Printf("[%s] %s\n", result.Version, properties.Title) 78 | 79 | err = debrid.AddMagnet(properties.Link, properties.Files) 80 | if err != nil { 81 | fmt.Println("Failed to add magnet. Skipping") 82 | fmt.Println(err) 83 | continue 84 | } 85 | } 86 | 87 | settings := config.GetSettings() 88 | host := settings.Plex.Host 89 | token := settings.Plex.Token 90 | movieId := settings.Plex.MovieId 91 | 92 | if host != "" && token != "" && movieId != "" { 93 | time.Sleep(20 * time.Second) 94 | err := plex.RefreshLibrary(movieId) 95 | if err != nil { 96 | fmt.Println("Failed to refresh library") 97 | fmt.Println(err) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /modules/overseerr/movies/main.go: -------------------------------------------------------------------------------- 1 | package movies 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "pod-link/modules/config" 8 | ) 9 | 10 | func GetDetails(id string) (Movie, error) { 11 | settings := config.GetSettings() 12 | host := settings.Overseerr.Host 13 | token := settings.Overseerr.Token 14 | 15 | url := fmt.Sprintf("%s/api/v1/movie/%s", host, id) 16 | 17 | req, err := http.NewRequest("GET", url, nil) 18 | if err != nil { 19 | fmt.Println("Failed to create request") 20 | return Movie{}, err 21 | } 22 | 23 | req.Header.Add("X-Api-Key", token) 24 | 25 | client := &http.Client{} 26 | 27 | response, err := client.Do(req) 28 | if err != nil { 29 | fmt.Println("Failed to send request") 30 | return Movie{}, err 31 | } 32 | 33 | defer response.Body.Close() 34 | 35 | var details Movie 36 | err = json.NewDecoder(response.Body).Decode(&details) 37 | if err != nil { 38 | fmt.Println("Failed to decode response") 39 | return Movie{}, err 40 | } 41 | 42 | return details, nil 43 | } 44 | -------------------------------------------------------------------------------- /modules/overseerr/movies/structs.go: -------------------------------------------------------------------------------- 1 | package movies 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Genre struct { 8 | Id int `json:"id"` 9 | Name string `json:"name"` 10 | } 11 | 12 | type Video struct { 13 | Url string `json:"url"` 14 | Key string `json:"key"` 15 | Name string `json:"name"` 16 | Size int `json:"size"` 17 | Type string `json:"type"` 18 | Site string `json:"site"` 19 | } 20 | 21 | type ProductionCompany struct { 22 | Id int `json:"id"` 23 | LogoPath string `json:"logoPath"` 24 | OriginCountry string `json:"originCountry"` 25 | Name string `json:"name"` 26 | } 27 | 28 | type ProductionCountry struct { 29 | Iso31661 string `json:"iso_3166_1"` 30 | Name string `json:"name"` 31 | } 32 | 33 | type ReleaseDate struct { 34 | Certification string `json:"certification"` 35 | Iso6391 string `json:"iso_639_1"` 36 | Note string `json:"note"` 37 | ReleaseDate time.Time `json:"release_date"` 38 | Type int `json:"type"` 39 | } 40 | 41 | type Cast struct { 42 | Id int `json:"id"` 43 | CastId int `json:"castId"` 44 | Character string `json:"character"` 45 | CreditId string `json:"creditId"` 46 | Gender int `json:"gender"` 47 | Name string `json:"name"` 48 | Order int `json:"order"` 49 | ProfilePath string `json:"profilePath"` 50 | } 51 | 52 | type Crew struct { 53 | Id int `json:"id"` 54 | CreditId string `json:"creditId"` 55 | Gender int `json:"gender"` 56 | Name string `json:"name"` 57 | Job string `json:"job"` 58 | Department string `json:"department"` 59 | ProfilePath string `json:"profilePath"` 60 | } 61 | 62 | type Collection struct { 63 | Id int `json:"id"` 64 | Name string `json:"name"` 65 | PosterPath string `json:"posterPath"` 66 | BackdropPath string `json:"backdropPath"` 67 | } 68 | 69 | type ExternalIds struct { 70 | FacebookId string `json:"facebookId"` 71 | FreebaseId string `json:"freebaseId"` 72 | FreebaseMid string `json:"freebaseMid"` 73 | ImdbId string `json:"imdbId"` 74 | InstagramId string `json:"instagramId"` 75 | TvdbId int `json:"tvdbId"` 76 | TvrageId int `json:"tvrageId"` 77 | TwitterId string `json:"twitterId"` 78 | } 79 | 80 | type RequestedBy struct { 81 | Id int `json:"id"` 82 | Email string `json:"email"` 83 | Username string `json:"username"` 84 | PlexToken string `json:"plexToken"` 85 | PlexUsername string `json:"plexUsername"` 86 | UserType int `json:"userType"` 87 | Permissions int `json:"permissions"` 88 | Avatar string `json:"avatar"` 89 | CreatedAt time.Time `json:"createdAt"` 90 | UpdatedAt time.Time `json:"updatedAt"` 91 | RequestCount int `json:"requestCount"` 92 | } 93 | 94 | type WatchProvider struct { 95 | Iso31661 string `json:"iso_3166_1"` 96 | Link string `json:"link"` 97 | Buy []struct { 98 | DisplayPriority int `json:"displayPriority"` 99 | LogoPath string `json:"logoPath"` 100 | Id int `json:"id"` 101 | Name string `json:"name"` 102 | } `json:"buy"` 103 | Flatrate []struct { 104 | DisplayPriority int `json:"displayPriority"` 105 | LogoPath string `json:"logoPath"` 106 | Id int `json:"id"` 107 | Name string `json:"name"` 108 | } `json:"flatrate"` 109 | } 110 | 111 | type Movie struct { 112 | Id int `json:"id"` 113 | ImdbId string `json:"imdbId"` 114 | Adult bool `json:"adult"` 115 | BackdropPath string `json:"backdropPath"` 116 | PosterPath string `json:"posterPath"` 117 | Budget int `json:"budget"` 118 | Genres []Genre `json:"genres"` 119 | Homepage string `json:"homepage"` 120 | RelatedVideos []Video `json:"relatedVideos"` 121 | OriginalLanguage string `json:"originalLanguage"` 122 | OriginalTitle string `json:"originalTitle"` 123 | Overview string `json:"overview"` 124 | Popularity float64 `json:"popularity"` 125 | ProductionCompanies []ProductionCompany `json:"productionCompanies"` 126 | ProductionCountries []ProductionCountry `json:"productionCountries"` 127 | ReleaseDate string `json:"releaseDate"` 128 | Releases struct { 129 | Results []struct { 130 | Iso31661 string `json:"iso_3166_1"` 131 | Rating string `json:"rating"` 132 | ReleaseDates []ReleaseDate `json:"release_dates"` 133 | } `json:"results"` 134 | } `json:"releases"` 135 | Revenue int `json:"revenue"` 136 | Runtime int `json:"runtime"` 137 | SpokenLanguages []struct { 138 | EnglishName string `json:"englishName"` 139 | Iso6391 string `json:"iso_639_1"` 140 | Name string `json:"name"` 141 | } `json:"spokenLanguages"` 142 | Status string `json:"status"` 143 | Tagline string `json:"tagline"` 144 | Title string `json:"title"` 145 | Video bool `json:"video"` 146 | VoteAverage float64 `json:"voteAverage"` 147 | VoteCount int `json:"voteCount"` 148 | Credits struct { 149 | Cast []Cast `json:"cast"` 150 | Crew []Crew `json:"crew"` 151 | } `json:"credits"` 152 | Collection Collection `json:"collection"` 153 | ExternalIds ExternalIds `json:"externalIds"` 154 | MediaInfo struct { 155 | ID int `json:"id"` 156 | TmdbID int `json:"tmdbId"` 157 | TvdbID int `json:"tvdbId"` 158 | Status int `json:"status"` 159 | CreatedAt string `json:"createdAt"` 160 | UpdatedAt string `json:"updatedAt"` 161 | } `json:"mediaInfo"` 162 | WatchProviders []struct { 163 | WatchProvider 164 | } `json:"watchProviders"` 165 | } 166 | -------------------------------------------------------------------------------- /modules/overseerr/tv/handler.go: -------------------------------------------------------------------------------- 1 | package tv 2 | 3 | import ( 4 | "fmt" 5 | "pod-link/modules/config" 6 | "pod-link/modules/debrid" 7 | "pod-link/modules/plex" 8 | "pod-link/modules/structs" 9 | "pod-link/modules/torrentio" 10 | torrentio_tv "pod-link/modules/torrentio/tv" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | func FindByEpisode(season int, episode int, details Tv, wg *sync.WaitGroup) { 16 | results, err := torrentio_tv.GetList(details.ExternalIds.ImdbID, season, episode) 17 | if err != nil { 18 | fmt.Printf("[S%vE%v] Failed to get results\n", season, episode) 19 | fmt.Println(err) 20 | wg.Done() 21 | return 22 | } 23 | 24 | episodes := torrentio_tv.FilterEpisodes(results) 25 | filtered := torrentio.FilterVersions(episodes, "shows") 26 | 27 | if len(filtered) == 0 { 28 | fmt.Printf("[S%vE%v] Not found\n", season, episode) 29 | wg.Done() 30 | return 31 | } 32 | 33 | for _, result := range filtered { 34 | properties, err := torrentio.GetPropertiesFromStream(result) 35 | if err != nil { 36 | fmt.Printf("[%s - S%vE%v] Failed to get properties\n", result.Version, season, episode) 37 | fmt.Println(err) 38 | continue 39 | } 40 | 41 | fmt.Printf("[%s - S%vE%v] + %v\n", result.Version, season, episode, properties.Title) 42 | 43 | err = debrid.AddMagnet(properties.Link, properties.Files) 44 | if err != nil { 45 | fmt.Printf("[%s - S%vE%v] Failed to add magnet\n", result.Version, season, episode) 46 | fmt.Println(err) 47 | continue 48 | } 49 | } 50 | 51 | wg.Done() 52 | } 53 | 54 | func FindBySeason(season int, details Tv, seasonWg *sync.WaitGroup) { 55 | results, err := torrentio_tv.GetList(details.ExternalIds.ImdbID, season, 1) 56 | if err != nil { 57 | fmt.Printf("[S%v] Failed to get results\n", season) 58 | fmt.Println(err) 59 | seasonWg.Done() 60 | return 61 | } 62 | 63 | 64 | seasons, err := torrentio_tv.FilterSeasons(results) 65 | if err != nil { 66 | fmt.Printf("[S%v] Failed to filter seasons\n", season) 67 | fmt.Println(err) 68 | seasonWg.Done() 69 | return 70 | } 71 | 72 | filtered := torrentio.FilterVersions(seasons, "shows") 73 | 74 | if len(filtered) == 0 { 75 | fmt.Printf("[S%v] No complete seasons found, searching for episodes\n", season) 76 | episodes := getEpisodeCountBySeason(season, details.Seasons) 77 | 78 | if episodes == 0 { 79 | fmt.Printf("[S%v] Failed to get episode count\n", season) 80 | seasonWg.Done() 81 | return 82 | } 83 | 84 | var episodesWg sync.WaitGroup 85 | for episode := 1; episode <= episodes; episode++ { 86 | episodesWg.Add(1) 87 | go FindByEpisode(season, episode, details, &episodesWg) 88 | } 89 | 90 | episodesWg.Wait() 91 | seasonWg.Done() 92 | return 93 | } 94 | 95 | for _, result := range filtered { 96 | properties, err := torrentio.GetPropertiesFromStream(result) 97 | if err != nil { 98 | fmt.Printf("[%s - S%v] Failed to get properties\n", result.Version, season) 99 | fmt.Println(err) 100 | continue 101 | } 102 | 103 | fmt.Printf("[%s - S%v] + %v\n", result.Version, season, properties.Title) 104 | 105 | err = debrid.AddMagnet(properties.Link, properties.Files) 106 | if err != nil { 107 | fmt.Printf("[%s - S%v] Failed to add magnet\n", result.Version, season) 108 | fmt.Println(err) 109 | continue 110 | } 111 | } 112 | 113 | seasonWg.Done() 114 | } 115 | 116 | func Request(notification structs.MediaAutoApprovedNotification) { 117 | details, err := GetDetails(notification.Media.TmdbId) 118 | if err != nil { 119 | fmt.Println("Failed to get details") 120 | fmt.Println(err) 121 | return 122 | } 123 | 124 | fmt.Println("Got request for", details.Name) 125 | 126 | seasons := getRequestedSeasons(notification.Extra) 127 | fmt.Println("Requested seasons:", seasons) 128 | 129 | if len(seasons) == 0 { 130 | fmt.Println("No seasons found") 131 | return 132 | } 133 | 134 | var seasonWg sync.WaitGroup 135 | for _, season := range seasons { 136 | seasonWg.Add(1) 137 | go FindBySeason(season, details, &seasonWg) 138 | } 139 | 140 | seasonWg.Wait() 141 | 142 | settings := config.GetSettings() 143 | host := settings.Plex.Host 144 | token := settings.Plex.Token 145 | tvId := settings.Plex.TvId 146 | 147 | if host != "" && token != "" && tvId != "" { 148 | time.Sleep(20 * time.Second) 149 | err := plex.RefreshLibrary(tvId) 150 | if err != nil { 151 | fmt.Println(err) 152 | fmt.Println("Failed to refresh library") 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /modules/overseerr/tv/main.go: -------------------------------------------------------------------------------- 1 | package tv 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "pod-link/modules/config" 8 | "pod-link/modules/structs" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | type OverseerrResponse struct { 14 | ExternalIds struct { 15 | ImdbId string `json:"imdbId"` 16 | } `json:"externalIds"` 17 | } 18 | 19 | func isAnime(keywords []Keyword) bool { 20 | for _, keyword := range keywords { 21 | if strings.ToLower(keyword.Name) == "anime" { 22 | return true 23 | } 24 | } 25 | 26 | return false 27 | } 28 | 29 | func getEpisodeCountBySeason(number int, seasons []Season) int { 30 | for _, season := range seasons { 31 | if season.SeasonNumber == number { 32 | return season.EpisodeCount 33 | } 34 | } 35 | 36 | return 0 37 | } 38 | 39 | func getRequestedSeasons(extra []structs.Extra) []int { 40 | var seasonNumbers = []int{} 41 | 42 | for _, extra := range extra { 43 | if extra.Name != "Requested Seasons" { 44 | continue 45 | } 46 | 47 | list := strings.Split(extra.Value, ", ") 48 | for _, season := range list { 49 | seasonNumber, err := strconv.Atoi(season) 50 | if err != nil { 51 | fmt.Println("Failed to convert season to int. Skipping") 52 | fmt.Println(err) 53 | continue 54 | } 55 | 56 | seasonNumbers = append(seasonNumbers, seasonNumber) 57 | } 58 | } 59 | 60 | return seasonNumbers 61 | } 62 | 63 | func GetDetails(id string) (Tv, error) { 64 | settings := config.GetSettings() 65 | host := settings.Overseerr.Host 66 | token := settings.Overseerr.Token 67 | url := fmt.Sprintf("%s/api/v1/tv/%s", host, id) 68 | 69 | req, err := http.NewRequest("GET", url, nil) 70 | if err != nil { 71 | fmt.Println(err) 72 | fmt.Println("Failed to create request") 73 | } 74 | 75 | req.Header.Add("X-Api-Key", token) 76 | 77 | client := &http.Client{} 78 | 79 | response, err := client.Do(req) 80 | if err != nil { 81 | fmt.Println("Failed to send request") 82 | return Tv{}, err 83 | } 84 | 85 | defer response.Body.Close() 86 | 87 | var data Tv 88 | err = json.NewDecoder(response.Body).Decode(&data) 89 | if err != nil { 90 | fmt.Println("Failed to decode response") 91 | return Tv{}, err 92 | } 93 | 94 | return data, nil 95 | } 96 | -------------------------------------------------------------------------------- /modules/overseerr/tv/structs.go: -------------------------------------------------------------------------------- 1 | package tv 2 | 3 | type ContentRating struct { 4 | Iso3166_1 string `json:"iso_3166_1"` 5 | Rating string `json:"rating"` 6 | } 7 | 8 | type CreatedBy struct { 9 | ID int `json:"id"` 10 | Name string `json:"name"` 11 | Gender int `json:"gender"` 12 | ProfilePath string `json:"profilePath"` 13 | } 14 | 15 | type LastEpisode struct { 16 | ID int `json:"id"` 17 | Name string `json:"name"` 18 | AirDate string `json:"airDate"` 19 | EpisodeNumber int `json:"episodeNumber"` 20 | Overview string `json:"overview"` 21 | ProductionCode string `json:"productionCode"` 22 | SeasonNumber int `json:"seasonNumber"` 23 | ShowID int `json:"showId"` 24 | StillPath string `json:"stillPath"` 25 | VoteAverage float64 `json:"voteAverage"` 26 | VoteCount int `json:"voteCount"` 27 | } 28 | 29 | type Season struct { 30 | ID int `json:"id"` 31 | AirDate string `json:"airDate"` 32 | EpisodeCount int `json:"episodeCount"` 33 | Name string `json:"name"` 34 | Overview string `json:"overview"` 35 | PosterPath string `json:"posterPath"` 36 | SeasonNumber int `json:"seasonNumber"` 37 | Episodes []LastEpisode `json:"episodes"` 38 | } 39 | 40 | type Cast struct { 41 | ID int `json:"id"` 42 | CastID int `json:"castId"` 43 | Character string `json:"character"` 44 | CreditID string `json:"creditId"` 45 | Gender int `json:"gender"` 46 | Name string `json:"name"` 47 | Order int `json:"order"` 48 | ProfilePath string `json:"profilePath"` 49 | } 50 | 51 | type Crew struct { 52 | ID int `json:"id"` 53 | CreditID string `json:"creditId"` 54 | Gender int `json:"gender"` 55 | Name string `json:"name"` 56 | Job string `json:"job"` 57 | Department string `json:"department"` 58 | ProfilePath string `json:"profilePath"` 59 | } 60 | 61 | type ExternalIds struct { 62 | FacebookID string `json:"facebookId"` 63 | FreebaseID string `json:"freebaseId"` 64 | FreebaseMid string `json:"freebaseMid"` 65 | ImdbID string `json:"imdbId"` 66 | InstagramID string `json:"instagramId"` 67 | TvdbID int `json:"tvdbId"` 68 | TvrageID int `json:"tvrageId"` 69 | TwitterID string `json:"twitterId"` 70 | } 71 | 72 | type MediaRequester struct { 73 | ID int `json:"id"` 74 | Status int `json:"status"` 75 | Media struct { 76 | DownloadStatus []interface{} `json:"downloadStatus"` 77 | DownloadStatus4k []interface{} `json:"downloadStatus4k"` 78 | ID int `json:"id"` 79 | MediaType string `json:"mediaType"` 80 | TmdbID int `json:"tmdbId"` 81 | TvdbID int `json:"tvdbId"` 82 | ImdbID string `json:"imdbId"` 83 | Status int `json:"status"` 84 | Status4k int `json:"status4k"` 85 | CreatedAt string `json:"createdAt"` 86 | UpdatedAt string `json:"updatedAt"` 87 | LastSeasonChange string `json:"lastSeasonChange"` 88 | MediaAddedAt string `json:"mediaAddedAt"` 89 | ServiceID int `json:"serviceId"` 90 | ServiceID4k int `json:"serviceId4k"` 91 | ExternalServiceID string `json:"externalServiceId"` 92 | ExternalServiceID4k string `json:"externalServiceId4k"` 93 | ExternalServiceSlug string `json:"externalServiceSlug"` 94 | ExternalServiceSlug4k string `json:"externalServiceSlug4k"` 95 | RatingKey string `json:"ratingKey"` 96 | RatingKey4k string `json:"ratingKey4k"` 97 | } `json:"media"` 98 | CreatedAt string `json:"createdAt"` 99 | UpdatedAt string `json:"updatedAt"` 100 | RequestedBy struct { 101 | ID int `json:"id"` 102 | Email string `json:"email"` 103 | Username string `json:"username"` 104 | PlexToken string `json:"plexToken"` 105 | PlexUsername string `json:"plexUsername"` 106 | UserType int `json:"userType"` 107 | Permissions int `json:"permissions"` 108 | Avatar string `json:"avatar"` 109 | CreatedAt string `json:"createdAt"` 110 | UpdatedAt string `json:"updatedAt"` 111 | RequestCount int `json:"requestCount"` 112 | } `json:"requestedBy"` 113 | ModifiedBy struct { 114 | ID int `json:"id"` 115 | Email string `json:"email"` 116 | Username string `json:"username"` 117 | PlexToken string `json:"plexToken"` 118 | PlexUsername string `json:"plexUsername"` 119 | UserType int `json:"userType"` 120 | Permissions int `json:"permissions"` 121 | Avatar string `json:"avatar"` 122 | CreatedAt string `json:"createdAt"` 123 | UpdatedAt string `json:"updatedAt"` 124 | RequestCount int `json:"requestCount"` 125 | } `json:"modifiedBy"` 126 | Is4k bool `json:"is4k"` 127 | ServerID int `json:"serverId"` 128 | ProfileID int `json:"profileId"` 129 | RootFolder string `json:"rootFolder"` 130 | } 131 | 132 | type Link struct { 133 | Iso3166_1 string `json:"iso_3166_1"` 134 | Link string `json:"link"` 135 | Buy []struct { 136 | DisplayPriority int `json:"displayPriority"` 137 | LogoPath string `json:"logoPath"` 138 | ID int `json:"id"` 139 | Name string `json:"name"` 140 | } `json:"buy"` 141 | FlatRate []struct { 142 | DisplayPriority int `json:"displayPriority"` 143 | LogoPath string `json:"logoPath"` 144 | ID int `json:"id"` 145 | Name string `json:"name"` 146 | } `json:"flatrate"` 147 | } 148 | 149 | type Keyword struct { 150 | ID int `json:"id"` 151 | Name string `json:"name"` 152 | } 153 | 154 | type Tv struct { 155 | ID int `json:"id"` 156 | BackdropPath string `json:"backdropPath"` 157 | PosterPath string `json:"posterPath"` 158 | ContentRatings ContentRating `json:"contentRatings"` 159 | CreatedBy []CreatedBy `json:"createdBy"` 160 | EpisodeRunTime []int `json:"episodeRunTime"` 161 | FirstAirDate string `json:"firstAirDate"` 162 | Genres []struct { 163 | ID int `json:"id"` 164 | Name string `json:"name"` 165 | } `json:"genres"` 166 | Homepage string `json:"homepage"` 167 | InProduction bool `json:"inProduction"` 168 | Languages []string `json:"languages"` 169 | LastAirDate string `json:"lastAirDate"` 170 | LastEpisodeToAir LastEpisode `json:"lastEpisodeToAir"` 171 | Name string `json:"name"` 172 | NextEpisodeToAir LastEpisode `json:"nextEpisodeToAir"` 173 | Networks []struct { 174 | ID int `json:"id"` 175 | LogoPath string `json:"logoPath"` 176 | OriginCountry string `json:"originCountry"` 177 | Name string `json:"name"` 178 | } `json:"networks"` 179 | NumberOfEpisodes int `json:"numberOfEpisodes"` 180 | NumberOfSeasons int `json:"numberOfSeason"` 181 | OriginCountry []string `json:"originCountry"` 182 | OriginalLanguage string `json:"originalLanguage"` 183 | OriginalName string `json:"originalName"` 184 | Overview string `json:"overview"` 185 | Popularity float64 `json:"popularity"` 186 | ProductionCompanies []struct { 187 | ID int `json:"id"` 188 | LogoPath string `json:"logoPath"` 189 | OriginCountry string `json:"originCountry"` 190 | Name string `json:"name"` 191 | } `json:"productionCompanies"` 192 | ProductionCountries []struct { 193 | Iso3166_1 string `json:"iso_3166_1"` 194 | Name string `json:"name"` 195 | } `json:"productionCountries"` 196 | SpokenLanguages []struct { 197 | EnglishName string `json:"englishName"` 198 | Iso639_1 string `json:"iso_639_1"` 199 | Name string `json:"name"` 200 | } `json:"spokenLanguages"` 201 | Seasons []Season `json:"seasons"` 202 | Status string `json:"status"` 203 | Tagline string `json:"tagline"` 204 | Type string `json:"type"` 205 | VoteAverage float64 `json:"voteAverage"` 206 | VoteCount int `json:"voteCount"` 207 | Credits struct { 208 | Cast []Cast `json:"cast"` 209 | Crew []Crew `json:"crew"` 210 | } `json:"credits"` 211 | ExternalIds ExternalIds `json:"externalIds"` 212 | Keywords []Keyword `json:"keywords"` 213 | MediaInfo struct { 214 | ID int `json:"id"` 215 | TmdbID int `json:"tmdbId"` 216 | TvdbID int `json:"tvdbId"` 217 | Status int `json:"status"` 218 | Requests []MediaRequester `json:"requests"` 219 | CreatedAt string `json:"createdAt"` 220 | UpdatedAt string `json:"updatedAt"` 221 | } `json:"mediaInfo"` 222 | WatchProviders []Link `json:"watchProviders"` 223 | } 224 | -------------------------------------------------------------------------------- /modules/plex/main.go: -------------------------------------------------------------------------------- 1 | package plex 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "pod-link/modules/config" 7 | ) 8 | 9 | func RefreshLibrary(id string) error { 10 | settings := config.GetSettings() 11 | host := settings.Plex.Host 12 | token := settings.Plex.Token 13 | 14 | url := fmt.Sprintf("%s/library/sections/%s/refresh?X-Plex-Token=%s", host, id, token) 15 | 16 | req, err := http.NewRequest("GET", url, nil) 17 | if err != nil { 18 | fmt.Println(err) 19 | fmt.Println("Failed to create request") 20 | return err 21 | } 22 | 23 | client := &http.Client{} 24 | 25 | response, err := client.Do(req) 26 | if err != nil { 27 | fmt.Println(err) 28 | fmt.Println("Failed to send request") 29 | return err 30 | } 31 | 32 | defer response.Body.Close() 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /modules/structs/main.go: -------------------------------------------------------------------------------- 1 | package structs 2 | 3 | type Extra struct { 4 | Name string `json:"name"` 5 | Value string `json:"value"` 6 | } 7 | 8 | type MediaAutoApprovedNotification struct { 9 | NotificationType string `json:"notification_type"` 10 | Media struct { 11 | MediaType string `json:"media_type"` 12 | TmdbId string `json:"tmdbId"` 13 | TvdId string `json:"tvdbId"` 14 | } 15 | Extra []Extra `json:"extra"` 16 | } 17 | -------------------------------------------------------------------------------- /modules/torrentio/anime/main.go: -------------------------------------------------------------------------------- 1 | package anime 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "pod-link/modules/config" 8 | "pod-link/modules/torrentio" 9 | ) 10 | 11 | func GetList(KitsuId string, Episode int) []torrentio.Stream { 12 | settings := config.GetSettings() 13 | filter := settings.Torrentio.FilterURI 14 | url := fmt.Sprintf("https://torrentio.strem.fun/%s/stream/anime/kitsu:%s:%v.json", filter, KitsuId, Episode) 15 | 16 | req, err := http.NewRequest("GET", url, nil) 17 | if err != nil { 18 | fmt.Println(err) 19 | fmt.Println("Failed to create request") 20 | } 21 | 22 | client := &http.Client{} 23 | 24 | response, err := client.Do(req) 25 | if err != nil { 26 | fmt.Println(err) 27 | fmt.Println("Failed to send request") 28 | } 29 | 30 | defer response.Body.Close() 31 | 32 | var data torrentio.Response 33 | err = json.NewDecoder(response.Body).Decode(&data) 34 | if err != nil { 35 | fmt.Println(err) 36 | fmt.Println("Failed to decode response") 37 | } 38 | 39 | return torrentio.FilterFormats(data.Streams, "anime") 40 | } 41 | -------------------------------------------------------------------------------- /modules/torrentio/main.go: -------------------------------------------------------------------------------- 1 | package torrentio 2 | 3 | import ( 4 | "fmt" 5 | "pod-link/modules/config" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | type Stream struct { 11 | Name string `json:"name"` 12 | Title string `json:"title"` 13 | Url string `json:"url"` 14 | Version string 15 | BehaviorHints struct { 16 | BingeGroup string `json:"bingeGroup"` 17 | } 18 | } 19 | 20 | type Response struct { 21 | Streams []Stream 22 | } 23 | 24 | type Properties struct { 25 | Title string 26 | Size string 27 | Link string 28 | Seeds string 29 | Source string 30 | Release string 31 | Files string 32 | } 33 | 34 | func GetBaseURL(mediaType string) string { 35 | settings := config.GetSettings() 36 | 37 | var filter string 38 | switch mediaType { 39 | case "shows": 40 | filter = settings.Torrentio.Shows.FilterURI 41 | case "movies": 42 | filter = settings.Torrentio.Movies.FilterURI 43 | default: 44 | filter = "" 45 | } 46 | 47 | token := settings.RealDebrid.Token 48 | url := fmt.Sprintf("https://torrentio.strem.fun/%s|realdebrid=%s", filter, token) 49 | 50 | return url 51 | } 52 | 53 | func getEmojiValues(input string) ([]string, error) { 54 | pattern := `👤\s(.*?)\s💾\s(.*?)\s⚙️\s(.*$)` 55 | 56 | regex, err := regexp.Compile(pattern) 57 | if err != nil { 58 | fmt.Printf("Error compiling regular expression: %v\n", err) 59 | return nil, err 60 | } 61 | 62 | return regex.FindStringSubmatch(input)[1:], nil 63 | } 64 | 65 | func getMagnet(input string) (string, string) { 66 | split := strings.Split(input, "/") 67 | hash := split[5] 68 | so := split[6] 69 | dn := split[8] 70 | 71 | if so == "null" { 72 | so = "all" 73 | } 74 | 75 | return "magnet:?xt=urn:btih:" + hash + "&dn=" + dn, so 76 | } 77 | 78 | func GetPropertiesFromStream(stream Stream) (Properties, error) { 79 | var properties Properties 80 | 81 | emojiString := "" 82 | 83 | titleSplit := strings.Split(stream.Title, "\n") 84 | for _, title := range titleSplit { 85 | if strings.Contains(title, "👤") && strings.Contains(title, "💾") && strings.Contains(title, "⚙️") { 86 | emojiString = title 87 | } 88 | } 89 | 90 | emojiValues, err := getEmojiValues(emojiString) 91 | if err != nil { 92 | fmt.Printf("Error getting emoji values: %v\n", err) 93 | return Properties{}, err 94 | } 95 | 96 | magnet, files := getMagnet(stream.Url) 97 | 98 | properties.Title = strings.ReplaceAll(titleSplit[0], " ", ".") 99 | properties.Link = magnet 100 | properties.Seeds = emojiValues[0] 101 | properties.Size = emojiValues[1] 102 | properties.Source = emojiValues[2] 103 | properties.Release = "[torrentio: " + properties.Source + "]" 104 | properties.Files = files 105 | 106 | return properties, nil 107 | } 108 | 109 | func getByVersion(version config.Version, streams []Stream) (Stream, error) { 110 | for _, stream := range streams { 111 | match := true 112 | 113 | for _, include := range version.Include { 114 | regex, err := regexp.Compile(include) 115 | if err != nil { 116 | fmt.Printf("Error compiling include regex for version: %v\n", version.Name) 117 | return Stream{}, err 118 | } 119 | 120 | if !regex.MatchString(stream.Title) { 121 | match = false 122 | break 123 | } 124 | } 125 | 126 | for _, exclude := range version.Exclude { 127 | regex, err := regexp.Compile(exclude) 128 | if err != nil { 129 | fmt.Printf("Error compiling exclude regex for version: %v\n", version.Name) 130 | return Stream{}, err 131 | } 132 | 133 | if regex.MatchString(stream.Title) { 134 | match = false 135 | break 136 | } 137 | } 138 | 139 | 140 | if match { 141 | return stream, nil 142 | } 143 | } 144 | 145 | return Stream{}, nil 146 | } 147 | 148 | func FilterVersions(streams []Stream, mediaType string) []Stream { 149 | var results []Stream 150 | 151 | versions := config.GetVersions(mediaType) 152 | 153 | for _, version := range versions { 154 | result, err := getByVersion(version, streams) 155 | if err != nil { 156 | fmt.Printf("Error getting version: %v\n", version.Name) 157 | fmt.Println(err) 158 | continue 159 | } 160 | 161 | if result == (Stream{}) { 162 | fmt.Printf("[%s] No match found\n", version.Name) 163 | continue 164 | } 165 | 166 | result.Version = version.Name 167 | results = append(results, result) 168 | } 169 | 170 | if len(results) == 0 && len(streams) > 0 { 171 | streams[0].Version = "Fallback" 172 | results = append(results, streams[0]) 173 | } 174 | 175 | return results 176 | } 177 | -------------------------------------------------------------------------------- /modules/torrentio/movies/main.go: -------------------------------------------------------------------------------- 1 | package movies 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "pod-link/modules/torrentio" 8 | ) 9 | 10 | func GetList(ImdbId string) ([]torrentio.Stream, error) { 11 | baseURL := torrentio.GetBaseURL("movies") 12 | url := fmt.Sprintf("%s/stream/movie/%s.json", baseURL, ImdbId) 13 | 14 | req, err := http.NewRequest("GET", url, nil) 15 | if err != nil { 16 | fmt.Println("Failed to create request") 17 | return nil, err 18 | } 19 | 20 | client := &http.Client{} 21 | 22 | response, err := client.Do(req) 23 | if err != nil { 24 | fmt.Println("Failed to send request") 25 | return nil, err 26 | } 27 | 28 | defer response.Body.Close() 29 | 30 | var data torrentio.Response 31 | err = json.NewDecoder(response.Body).Decode(&data) 32 | if err != nil { 33 | fmt.Println("Failed to decode response") 34 | return nil, err 35 | } 36 | 37 | return data.Streams, nil 38 | } 39 | -------------------------------------------------------------------------------- /modules/torrentio/tv/main.go: -------------------------------------------------------------------------------- 1 | package tv 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "pod-link/modules/config" 8 | "pod-link/modules/torrentio" 9 | "regexp" 10 | "strings" 11 | ) 12 | 13 | func GetList(ImdbId string, Season int, Episode int) ([]torrentio.Stream, error) { 14 | baseURL := torrentio.GetBaseURL("shows") 15 | url := fmt.Sprintf("%s/stream/series/%s:%v:%v.json", baseURL, ImdbId, Season, Episode) 16 | 17 | req, err := http.NewRequest("GET", url, nil) 18 | if err != nil { 19 | fmt.Println("Failed to create request") 20 | return nil, err 21 | } 22 | 23 | client := &http.Client{} 24 | 25 | response, err := client.Do(req) 26 | if err != nil { 27 | fmt.Println("Failed to send request") 28 | return nil, err 29 | } 30 | 31 | defer response.Body.Close() 32 | 33 | var data torrentio.Response 34 | err = json.NewDecoder(response.Body).Decode(&data) 35 | if err != nil { 36 | fmt.Println("Failed to decode response") 37 | return nil, err 38 | } 39 | 40 | return data.Streams, nil 41 | } 42 | 43 | func FilterSeasons(streams []torrentio.Stream) ([]torrentio.Stream, error) { 44 | var results []torrentio.Stream 45 | 46 | config := config.GetConfig() 47 | 48 | for i, stream := range streams { 49 | var isSeasonMatch bool 50 | var isEpisodeMatch bool 51 | 52 | for _, season := range config.Shows.Seasons { 53 | regex, err := regexp.Compile(season) 54 | if err != nil { 55 | fmt.Println("Error compiling regular expression") 56 | return nil, err 57 | } 58 | 59 | isSeasonMatch = regex.MatchString(stream.Title) 60 | 61 | if isSeasonMatch { 62 | break 63 | } 64 | } 65 | 66 | for _, episode := range config.Shows.Episodes { 67 | regex, err := regexp.Compile(episode) 68 | if err != nil { 69 | fmt.Println("Error compiling regular expression") 70 | return nil, err 71 | } 72 | 73 | isEpisodeMatch = regex.MatchString(stream.Title) 74 | 75 | if isEpisodeMatch { 76 | break 77 | } 78 | } 79 | 80 | if isSeasonMatch && !isEpisodeMatch { 81 | results = append(results, streams[i]) 82 | } 83 | } 84 | 85 | return results, nil 86 | } 87 | 88 | func FilterEpisodes(streams []torrentio.Stream) []torrentio.Stream { 89 | var results []torrentio.Stream 90 | 91 | settings := config.GetSettings() 92 | token := settings.RealDebrid.Token 93 | 94 | for i, stream := range streams { 95 | url := strings.ReplaceAll(stream.Url, "https://torrentio.strem.fun/realdebrid/", "") 96 | url = strings.ReplaceAll(url, token, "") 97 | 98 | split := strings.Split(url, "/") 99 | if (split[2] == "1" || split[2] == "null") { 100 | results = append(results, streams[i]) 101 | } 102 | } 103 | 104 | return results 105 | } 106 | --------------------------------------------------------------------------------