diff --git a/src/Jackett.Common/Definitions/mteamtp.yml b/src/Jackett.Common/Definitions/mteamtp.yml deleted file mode 100644 index 7960aa2dc..000000000 --- a/src/Jackett.Common/Definitions/mteamtp.yml +++ /dev/null @@ -1,222 +0,0 @@ ---- -id: mteamtp -name: M-Team - TP -description: "M-Team TP (MTTP) is a CHINESE Private Torrent Tracker for HD MOVIES / TV / 3X" -language: zh-CN -type: private -encoding: UTF-8 -requestDelay: 5 -links: - - https://kp.m-team.cc/ - - https://tp.m-team.cc/ - - https://pt.m-team.cc/ - -caps: - categorymappings: - - {id: 401, cat: Movies/SD, desc: "Movie(電影)/SD", default: true} - - {id: 419, cat: Movies/HD, desc: "Movie(電影)/HD", default: true} - - {id: 420, cat: Movies/DVD, desc: "Movie(電影)/DVDiSo", default: true} - - {id: 421, cat: Movies/BluRay, desc: "Movie(電影)/Blu-Ray", default: true} - - {id: 439, cat: Movies/Other, desc: "Movie(電影)/Remux", default: true} - - {id: 403, cat: TV/SD, desc: "TV Series(影劇/綜藝)/SD", default: true} - - {id: 402, cat: TV/HD, desc: "TV Series(影劇/綜藝)/HD", default: true} - - {id: 435, cat: TV/SD, desc: "TV Series(影劇/綜藝)/DVDiSo", default: true} - - {id: 438, cat: TV/HD, desc: "TV Series(影劇/綜藝)/BD", default: true} - - {id: 404, cat: TV/Documentary, desc: "紀錄教育", default: true} - - {id: 405, cat: TV/Anime, desc: "Anime(動畫)", default: true} - - {id: 407, cat: TV/Sport, desc: "Sports(運動)", default: true} - - {id: 422, cat: PC/0day, desc: "Software(軟體)", default: true} - - {id: 423, cat: PC/Games, desc: "PCGame(PC遊戲)", default: true} - - {id: 427, cat: Books, desc: "eBook(電子書)", default: true} - - {id: 409, cat: Other, desc: "Misc(其他)", default: true} - # music - - {id: 406, cat: Audio/Video, desc: "MV(演唱)", default: true} - - {id: 408, cat: Audio/Other, desc: "Music(AAC/ALAC)", default: true} - - {id: 434, cat: Audio, desc: "Music(無損)", default: true} - # adult - - {id: 410, cat: XXX, desc: "AV(有碼)/HD Censored", default: false} - - {id: 429, cat: XXX, desc: "AV(無碼)/HD Uncensored", default: false} - - {id: 424, cat: XXX, desc: "AV(有碼)/SD Censored", default: false} - - {id: 430, cat: XXX, desc: "AV(無碼)/SD Uncensored", default: false} - - {id: 426, cat: XXX, desc: "AV(無碼)/DVDiSo Uncensored", default: false} - - {id: 437, cat: XXX, desc: "AV(有碼)/DVDiSo Censored", default: false} - - {id: 431, cat: XXX, desc: "AV(有碼)/Blu-Ray Censored", default: false} - - {id: 432, cat: XXX, desc: "AV(無碼)/Blu-Ray Uncensored", default: false} - - {id: 436, cat: XXX, desc: "AV(網站)/0Day", default: false} - - {id: 425, cat: XXX, desc: "IV(寫真影集)/Video Collection", default: false} - - {id: 433, cat: XXX, desc: "IV(寫真圖集)/Picture Collection", default: false} - - {id: 411, cat: XXX, desc: "H-Game(遊戲)", default: false} - - {id: 412, cat: XXX, desc: "H-Anime(動畫)", default: false} - - {id: 413, cat: XXX, desc: "H-Comic(漫畫)", default: false} - - modes: - search: [q] - tv-search: [q, season, ep, imdbid] - movie-search: [q, imdbid] - music-search: [q] - book-search: [q] - -settings: - - name: username - type: text - label: Username - - name: password - type: password - label: Password - - name: freeleech - type: checkbox - label: Search freeleech only - default: false - - name: sort - type: select - label: Sort requested from site - default: 4 - options: - 4: created - 7: seeders - 5: size - 1: title - - name: type - type: select - label: Order requested from site - default: desc - options: - desc: desc - asc: asc - - name: info_tpp - type: info - label: Results Per Page - default: For best results, change the Torrents per page: setting to 100 on your account profile. - - name: info_title - type: info - label: About Titles - default: For best results, disable the torrent name tooltip in User CP/Tracker Settings/Torrents Page. Otherwise long release names will be cut off. - - name: info_download_link - type: info - label: About Download Links - default: For best results, you must enable the Download icon in User CP/Tracker Settings/Torrents Page. - -login: - path: takelogin.php - method: post - inputs: - username: "{{ .Config.username }}" - password: "{{ .Config.password }}" - error: - - selector: td.embedded:has(h2:contains("登录失败")) - - selector: td.embedded:has(h2:contains("failed")) - - selector: td.toolbox:contains("錯誤") - - selector: td.toolbox:contains("Error") - - selector: td.toolbox:contains("限制登") - test: - path: index.php - selector: a[href="logout.php"] - -search: - paths: - - path: torrents.php - categories: [401, 419, 420, 421, 439, 403, 402, 435, 438, 404, 405, 407, 422, 423, 427, 409] - - path: adult.php - categories: [410, 429, 424, 430, 426, 437, 431, 432, 436, 425, 433, 411, 412, 413] - - path: music.php - categories: [406, 408, 434] - allowEmptyInputs: true - inputs: - $raw: "{{ range .Categories }}cat{{.}}=1&{{end}}" - search: "{{ if .Query.IMDBID }}{{ .Query.IMDBID }}{{ else }}{{ .Keywords }}{{ end }}" - # 0 incldead, 1 active, 2 dead - incldead: 0 - # 0 all, 1 normal, 2 free, 3 2x, 4 2xfree, 5 50%, 6 2x50%, 7 30% - spstate: "{{ if .Config.freeleech }}2{{ else }}0{{ end }}" - # 0 title, 3 uploader, 4 imdb url - search_area: "{{ if .Query.IMDBID }}4{{ else }}0{{ end }}" - # 0 AND, 1 OR, 2 exact - search_mode: 0 - sort: "{{ .Config.sort }}" - type: "{{ .Config.type }}" - notnewword: 1 - - rows: - selector: table.torrents > tbody > tr:has(table.torrentname) - - fields: - title_default: - # shortened for long release names - selector: a[href^="details.php?id="] > b - title: - # not available if IMDB tooltips are turned on - selector: a[title][href^="details.php?id="] - attribute: title - optional: true - default: "{{ .Result.title_default }}" - category: - selector: a[href^="?cat="] - attribute: href - filters: - - name: querystring - args: cat - details: - selector: a[href^="details.php?id="] - attribute: href - download: - selector: a[href^="download.php?id="] - attribute: href - poster: - selector: img[alt="torrent thumbnail"][src] - attribute: src - filters: - - name: replace - args: ["pic/nopic.jpg", ""] - imdbid: - selector: a[href*="imdb.com/title/tt"] - attribute: href - size: - selector: td.rowfollow:nth-last-child(6) - grabs: - selector: td.rowfollow:nth-last-child(3) - seeders: - selector: td.rowfollow:nth-last-child(5) - leechers: - selector: td.rowfollow:nth-last-child(4) - date_added: - selector: td.rowfollow:nth-last-child(7) > span[title] - optional: true - attribute: title - filters: - - name: append - args: " +08:00" # CST - - name: dateparse - args: "yyyy-MM-dd HH:mm:ss zzz" - date_elapsed: - selector: td.rowfollow:nth-last-child(7):not(:has(span)) - optional: true - filters: - - name: append - args: " +08:00" # CST - - name: dateparse - args: "yyyy-MM-ddHH:mm:ss zzz" - date: - text: "{{ if or .Result.date_elapsed .Result.date_added }}{{ or .Result.date_elapsed .Result.date_added }}{{ else }}now{{ end }}" - downloadvolumefactor: - case: - img.pro_free: 0 - img.pro_free2up: 0 - img.pro_50pctdown: 0.5 - img.pro_50pctdown2up: 0.5 - img.pro_30pctdown: 0.3 - "*": 1 - uploadvolumefactor: - case: - img.pro_50pctdown2up: 2 - img.pro_free2up: 2 - img.pro_2up: 2 - "*": 1 - minimumratio: - text: 1 - minimumseedtime: - # 2 days (as seconds = 2 x 24 x 60 x 60) - text: 172800 - description: - selector: td:nth-child(2) - remove: a, b, font, img, span -# NexusPHP Standard v1.5 Beta 4 diff --git a/src/Jackett.Common/Definitions/mteamtp2fa.yml b/src/Jackett.Common/Definitions/mteamtp2fa.yml deleted file mode 100644 index 10605468b..000000000 --- a/src/Jackett.Common/Definitions/mteamtp2fa.yml +++ /dev/null @@ -1,225 +0,0 @@ ---- -id: mteamtp2fa -name: M-Team - TP (2FA) -description: "This indexer uses a cookie login for M-Team TP (MTTP) for those that want to use 2FA" -language: zh-CN -type: private -encoding: UTF-8 -requestDelay: 5 -links: - - https://kp.m-team.cc/ - - https://tp.m-team.cc/ - - https://pt.m-team.cc/ - -caps: - categorymappings: - - {id: 401, cat: Movies/SD, desc: "Movie(電影)/SD", default: true} - - {id: 419, cat: Movies/HD, desc: "Movie(電影)/HD", default: true} - - {id: 420, cat: Movies/DVD, desc: "Movie(電影)/DVDiSo", default: true} - - {id: 421, cat: Movies/BluRay, desc: "Movie(電影)/Blu-Ray", default: true} - - {id: 439, cat: Movies/Other, desc: "Movie(電影)/Remux", default: true} - - {id: 403, cat: TV/SD, desc: "TV Series(影劇/綜藝)/SD", default: true} - - {id: 402, cat: TV/HD, desc: "TV Series(影劇/綜藝)/HD", default: true} - - {id: 435, cat: TV/SD, desc: "TV Series(影劇/綜藝)/DVDiSo", default: true} - - {id: 438, cat: TV/HD, desc: "TV Series(影劇/綜藝)/BD", default: true} - - {id: 404, cat: TV/Documentary, desc: "紀錄教育", default: true} - - {id: 405, cat: TV/Anime, desc: "Anime(動畫)", default: true} - - {id: 407, cat: TV/Sport, desc: "Sports(運動)", default: true} - - {id: 422, cat: PC/0day, desc: "Software(軟體)", default: true} - - {id: 423, cat: PC/Games, desc: "PCGame(PC遊戲)", default: true} - - {id: 427, cat: Books, desc: "eBook(電子書)", default: true} - - {id: 409, cat: Other, desc: "Misc(其他)", default: true} - # music - - {id: 406, cat: Audio/Video, desc: "MV(演唱)", default: true} - - {id: 408, cat: Audio/Other, desc: "Music(AAC/ALAC)", default: true} - - {id: 434, cat: Audio, desc: "Music(無損)", default: true} - # adult - - {id: 410, cat: XXX, desc: "AV(有碼)/HD Censored", default: false} - - {id: 429, cat: XXX, desc: "AV(無碼)/HD Uncensored", default: false} - - {id: 424, cat: XXX, desc: "AV(有碼)/SD Censored", default: false} - - {id: 430, cat: XXX, desc: "AV(無碼)/SD Uncensored", default: false} - - {id: 426, cat: XXX, desc: "AV(無碼)/DVDiSo Uncensored", default: false} - - {id: 437, cat: XXX, desc: "AV(有碼)/DVDiSo Censored", default: false} - - {id: 431, cat: XXX, desc: "AV(有碼)/Blu-Ray Censored", default: false} - - {id: 432, cat: XXX, desc: "AV(無碼)/Blu-Ray Uncensored", default: false} - - {id: 436, cat: XXX, desc: "AV(網站)/0Day", default: false} - - {id: 425, cat: XXX, desc: "IV(寫真影集)/Video Collection", default: false} - - {id: 433, cat: XXX, desc: "IV(寫真圖集)/Picture Collection", default: false} - - {id: 411, cat: XXX, desc: "H-Game(遊戲)", default: false} - - {id: 412, cat: XXX, desc: "H-Anime(動畫)", default: false} - - {id: 413, cat: XXX, desc: "H-Comic(漫畫)", default: false} - - modes: - search: [q] - tv-search: [q, season, ep, imdbid] - movie-search: [q, imdbid] - music-search: [q] - book-search: [q] - -settings: - - name: cookie - type: text - label: Cookie - - name: infocookie - type: info - label: How to get the Cookie - default: "
  1. Login to this tracker with your browser
  2. Open the DevTools panel by pressing F12
  3. Select the Network tab
  4. Click on the Doc button (Chrome Browser) or HTML button (FireFox)
  5. Refresh the page by pressing F5
  6. Click on the first row entry
  7. Select the Headers tab on the Right panel
  8. Find 'cookie:' in the Request Headers section
  9. Select and Copy the whole cookie string (everything after 'cookie: ') and Paste here.
" - - name: useragent - type: text - label: User-Agent - - name: info_useragent - type: info - label: How to get the User-Agent - default: "
  1. From the same place you fetched the cookie,
  2. Find 'user-agent:' in the Request Headers section
  3. Select and Copy the whole user-agent string (everything after 'user-agent: ') and Paste here.
" - - name: freeleech - type: checkbox - label: Search freeleech only - default: false - - name: sort - type: select - label: Sort requested from site - default: 4 - options: - 4: created - 7: seeders - 5: size - 1: title - - name: type - type: select - label: Order requested from site - default: desc - options: - desc: desc - asc: asc - - name: info_tpp - type: info - label: Results Per Page - default: For best results, change the Torrents per page: setting to 100 on your account profile. - - name: info_title - type: info - label: About Titles - default: For best results, disable the torrent name tooltip in User CP/Tracker Settings/Torrents Page. Otherwise long release names will be cut off. - - name: info_download_link - type: info - label: About Download Links - default: For best results, you must enable the Download icon in User CP/Tracker Settings/Torrents Page. - -login: - method: cookie - inputs: - cookie: "{{ .Config.cookie }}" - test: - path: index.php - selector: a[href="logout.php"] - -search: - paths: - - path: torrents.php - categories: [401, 419, 420, 421, 439, 403, 402, 435, 438, 404, 405, 407, 422, 423, 427, 409] - - path: adult.php - categories: [410, 429, 424, 430, 426, 437, 431, 432, 436, 425, 433, 411, 412, 413] - - path: music.php - categories: [406, 408, 434] - allowEmptyInputs: true - inputs: - $raw: "{{ range .Categories }}cat{{.}}=1&{{end}}" - search: "{{ if .Query.IMDBID }}{{ .Query.IMDBID }}{{ else }}{{ .Keywords }}{{ end }}" - # 0 incldead, 1 active, 2 dead - incldead: 0 - # 0 all, 1 normal, 2 free, 3 2x, 4 2xfree, 5 50%, 6 2x50%, 7 30% - spstate: "{{ if .Config.freeleech }}2{{ else }}0{{ end }}" - # 0 title, 3 uploader, 4 imdb url - search_area: "{{ if .Query.IMDBID }}4{{ else }}0{{ end }}" - # 0 AND, 1 OR, 2 exact - search_mode: 0 - sort: "{{ .Config.sort }}" - type: "{{ .Config.type }}" - notnewword: 1 - - headers: - User-Agent: ["{{ .Config.useragent }}"] - - rows: - selector: table.torrents > tbody > tr:has(table.torrentname) - - fields: - title_default: - # shortened for long release names - selector: a[href^="details.php?id="] > b - title: - # not available if IMDB tooltips are turned on - selector: a[title][href^="details.php?id="] - attribute: title - optional: true - default: "{{ .Result.title_default }}" - category: - selector: a[href^="?cat="] - attribute: href - filters: - - name: querystring - args: cat - details: - selector: a[href^="details.php?id="] - attribute: href - download: - selector: a[href^="download.php?id="] - attribute: href - poster: - selector: img[alt="torrent thumbnail"][src] - attribute: src - filters: - - name: replace - args: ["pic/nopic.jpg", ""] - imdbid: - selector: a[href*="imdb.com/title/tt"] - attribute: href - size: - selector: td.rowfollow:nth-last-child(6) - grabs: - selector: td.rowfollow:nth-last-child(3) - seeders: - selector: td.rowfollow:nth-last-child(5) - leechers: - selector: td.rowfollow:nth-last-child(4) - date_added: - selector: td.rowfollow:nth-last-child(7) > span[title] - optional: true - attribute: title - filters: - - name: append - args: " +08:00" # CST - - name: dateparse - args: "yyyy-MM-dd HH:mm:ss zzz" - date_elapsed: - selector: td.rowfollow:nth-last-child(7):not(:has(span)) - optional: true - filters: - - name: append - args: " +08:00" # CST - - name: dateparse - args: "yyyy-MM-ddHH:mm:ss zzz" - date: - text: "{{ if or .Result.date_elapsed .Result.date_added }}{{ or .Result.date_elapsed .Result.date_added }}{{ else }}now{{ end }}" - downloadvolumefactor: - case: - img.pro_free: 0 - img.pro_free2up: 0 - img.pro_50pctdown: 0.5 - img.pro_50pctdown2up: 0.5 - img.pro_30pctdown: 0.3 - "*": 1 - uploadvolumefactor: - case: - img.pro_50pctdown2up: 2 - img.pro_free2up: 2 - img.pro_2up: 2 - "*": 1 - minimumratio: - text: 1 - minimumseedtime: - # 2 days (as seconds = 2 x 24 x 60 x 60) - text: 172800 - description: - selector: td:nth-child(2) - remove: a, b, font, img, span -# NexusPHP Standard v1.5 Beta 4 diff --git a/src/Jackett.Common/Indexers/MTeamTp.cs b/src/Jackett.Common/Indexers/MTeamTp.cs new file mode 100644 index 000000000..45a7ecc1e --- /dev/null +++ b/src/Jackett.Common/Indexers/MTeamTp.cs @@ -0,0 +1,367 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Jackett.Common.Extensions; +using Jackett.Common.Models; +using Jackett.Common.Models.IndexerConfig.Bespoke; +using Jackett.Common.Serializer; +using Jackett.Common.Services.Interfaces; +using Jackett.Common.Utils; +using Jackett.Common.Utils.Clients; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NLog; +using WebClient = Jackett.Common.Utils.Clients.WebClient; + +namespace Jackett.Common.Indexers +{ + [ExcludeFromCodeCoverage] + public class MTeamTp : IndexerBase + { + public override string Id => "mteamtp"; + public override string Name => "M-Team - TP"; + public override string Description => "M-Team TP (MTTP) is a CHINESE Private Torrent Tracker for HD MOVIES / TV / 3X"; + public override string SiteLink { get; protected set; } = "https://kp.m-team.cc/"; + public override string[] AlternativeSiteLinks => new[] + { + "https://kp.m-team.cc/", + "https://tp.m-team.cc/", + "https://pt.m-team.cc/" + }; + public override string Language => "zh-CN"; + public override string Type => "private"; + + public override TorznabCapabilities TorznabCaps => SetCapabilities(); + + private readonly int[] _trackerAdultCategories = { 410, 429, 424, 430, 426, 437, 431, 432, 436, 425, 433, 411, 412, 413, 440 }; + + private new ConfigurationDataMTeamTp configData => (ConfigurationDataMTeamTp)base.configData; + + public MTeamTp(IIndexerConfigurationService configService, WebClient client, Logger logger, IProtectionService p, ICacheService cs) + : base(configService: configService, + client: client, + logger: logger, + p: p, + cacheService: cs, + configData: new ConfigurationDataMTeamTp()) + { + webclient.requestDelay = 5; + } + + private static TorznabCapabilities SetCapabilities() + { + var caps = new TorznabCapabilities + { + TvSearchParams = new List + { + TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId + }, + MovieSearchParams = new List + { + MovieSearchParam.Q, MovieSearchParam.ImdbId + }, + MusicSearchParams = new List + { + MusicSearchParam.Q + }, + BookSearchParams = new List + { + BookSearchParam.Q + } + }; + + caps.Categories.AddCategoryMapping(401, TorznabCatType.MoviesSD, "Movie(電影)/SD"); + caps.Categories.AddCategoryMapping(419, TorznabCatType.MoviesHD, "Movie(電影)/HD"); + caps.Categories.AddCategoryMapping(420, TorznabCatType.MoviesDVD, "Movie(電影)/DVDiSo"); + caps.Categories.AddCategoryMapping(421, TorznabCatType.MoviesBluRay, "Movie(電影)/Blu-Ray"); + caps.Categories.AddCategoryMapping(439, TorznabCatType.MoviesHD, "Movie(電影)/Remux"); + caps.Categories.AddCategoryMapping(403, TorznabCatType.TVSD, "TV Series(影劇/綜藝)/SD"); + caps.Categories.AddCategoryMapping(402, TorznabCatType.TVHD, "TV Series(影劇/綜藝)/HD"); + caps.Categories.AddCategoryMapping(435, TorznabCatType.TVSD, "TV Series(影劇/綜藝)/DVDiSo"); + caps.Categories.AddCategoryMapping(438, TorznabCatType.TVHD, "TV Series(影劇/綜藝)/BD"); + caps.Categories.AddCategoryMapping(404, TorznabCatType.TVDocumentary, "紀錄教育"); + caps.Categories.AddCategoryMapping(405, TorznabCatType.TVAnime, "Anime(動畫)"); + caps.Categories.AddCategoryMapping(407, TorznabCatType.TVSport, "Sports(運動)"); + caps.Categories.AddCategoryMapping(422, TorznabCatType.PC0day, "Software(軟體)"); + caps.Categories.AddCategoryMapping(423, TorznabCatType.PCGames, "PCGame(PC遊戲)"); + caps.Categories.AddCategoryMapping(427, TorznabCatType.Books, "eBook(電子書)"); + caps.Categories.AddCategoryMapping(409, TorznabCatType.Other, "Misc(其他)"); + + // music + caps.Categories.AddCategoryMapping(406, TorznabCatType.AudioVideo, "MV(演唱)"); + caps.Categories.AddCategoryMapping(408, TorznabCatType.AudioOther, "Music(AAC/ALAC)"); + caps.Categories.AddCategoryMapping(434, TorznabCatType.Audio, "Music(無損)"); + + // adult + caps.Categories.AddCategoryMapping(410, TorznabCatType.XXX, "AV(有碼)/HD Censored"); + caps.Categories.AddCategoryMapping(429, TorznabCatType.XXX, "AV(無碼)/HD Uncensored"); + caps.Categories.AddCategoryMapping(424, TorznabCatType.XXXSD, "AV(有碼)/SD Censored"); + caps.Categories.AddCategoryMapping(430, TorznabCatType.XXXSD, "AV(無碼)/SD Uncensored"); + caps.Categories.AddCategoryMapping(426, TorznabCatType.XXXDVD, "AV(無碼)/DVDiSo Uncensored"); + caps.Categories.AddCategoryMapping(437, TorznabCatType.XXXDVD, "AV(有碼)/DVDiSo Censored"); + caps.Categories.AddCategoryMapping(431, TorznabCatType.XXX, "AV(有碼)/Blu-Ray Censored"); + caps.Categories.AddCategoryMapping(432, TorznabCatType.XXX, "AV(無碼)/Blu-Ray Uncensored"); + caps.Categories.AddCategoryMapping(436, TorznabCatType.XXX, "AV(網站)/0Day"); + caps.Categories.AddCategoryMapping(425, TorznabCatType.XXX, "IV(寫真影集)/Video Collection"); + caps.Categories.AddCategoryMapping(433, TorznabCatType.XXXImageSet, "IV(寫真圖集)/Picture Collection"); + caps.Categories.AddCategoryMapping(411, TorznabCatType.XXX, "H-Game(遊戲)"); + caps.Categories.AddCategoryMapping(412, TorznabCatType.XXX, "H-Anime(動畫)"); + caps.Categories.AddCategoryMapping(413, TorznabCatType.XXX, "H-Comic(漫畫)"); + caps.Categories.AddCategoryMapping(440, TorznabCatType.XXX, "AV(Gay)/HD"); + + return caps; + } + + public override async Task ApplyConfiguration(JToken configJson) + { + LoadValuesFromJson(configJson); + + if (configData.ApiKey.Value.IsNullOrWhiteSpace()) + { + throw new Exception("Missing API Key."); + } + + var releases = await PerformQuery(new TorznabQuery()); + + await ConfigureIfOK(string.Empty, releases.Any(), + () => throw new Exception("Could not find releases.")); + + return IndexerConfigurationStatus.Completed; + } + + public override async Task Download(Uri link) + { + var response = await RequestWithCookiesAsync( + link.ToString(), + method: RequestType.POST, + headers: new Dictionary + { + { "Accept", "application/json" }, + { "x-api-key", configData.ApiKey.Value } + }); + + if (!STJson.TryDeserialize(response.ContentString, out var jsonResponse)) + { + throw new Exception("Invalid response received from M-Team, not a valid JSON"); + } + + if (jsonResponse.Data.IsNullOrWhiteSpace()) + { + throw new Exception($"Unable to find download link for: {link}"); + } + + return await base.Download(new Uri(jsonResponse.Data)); + } + + protected override async Task> PerformQuery(TorznabQuery query) + { + var releases = new List(); + + var categoryMapping = MapTorznabCapsToTrackers(query).Select(int.Parse).Distinct().ToList(); + + var adultCategories = categoryMapping.Where(c => _trackerAdultCategories.Contains(c)).ToList(); + var normalCategories = categoryMapping.Except(adultCategories).ToList(); + + if (!categoryMapping.Any() || normalCategories.Any()) + { + releases.AddRange(await FetchTrackerReleasesAsync(MTeamTpRequestType.Normal, query, normalCategories)); + } + + if (adultCategories.Any()) + { + releases.AddRange(await FetchTrackerReleasesAsync(MTeamTpRequestType.Adult, query, adultCategories)); + } + + return releases + .OrderByDescending(o => o.PublishDate) + .ToArray(); + } + + private async Task> FetchTrackerReleasesAsync(MTeamTpRequestType requestType, TorznabQuery query, IEnumerable categories) + { + var releases = new List(); + + var searchQuery = new MTeamTpApiSearchQuery + { + Mode = requestType, + Categories = categories?.Select(x => x.ToString()).ToArray() ?? Array.Empty(), + PageNumber = 1, + PageSize = 100 + }; + + if (query.ImdbID.IsNotNullOrWhiteSpace()) + { + searchQuery.Imdb = query.ImdbID.Trim(); + } + + var searchTerm = query.GetQueryString(); + + if (searchTerm.IsNotNullOrWhiteSpace()) + { + searchQuery.Keyword = searchTerm; + } + + if (configData.FreeleechOnly.Value) + { + searchQuery.Discount = "FREE"; + } + + var response = await RequestWithCookiesAndRetryAsync( + $"{SiteLink.TrimEnd('/')}/api/torrent/search", + method: RequestType.POST, + rawbody: STJson.ToJson(searchQuery), + headers: new Dictionary + { + { "Accept", "application/json" }, + { "Content-Type", "application/json" }, + { "x-api-key", configData.ApiKey.Value } + }); + + if (response.Status != HttpStatusCode.OK) + { + throw new Exception($"Unknown status code: {(int)response.Status} ({response.Status})"); + } + + if (!STJson.TryDeserialize(response.ContentString, out var jsonResponse)) + { + throw new Exception("Invalid response received from M-Team, not a valid JSON"); + } + + if (jsonResponse?.Data?.Torrents == null) + { + return releases; + } + + foreach (var torrent in jsonResponse.Data.Torrents) + { + var torrentId = int.Parse(torrent.Id); + var infoUrl = new Uri($"{SiteLink.TrimEnd('/')}/detail/{torrentId}"); + var downloadUrl = new Uri($"{SiteLink.TrimEnd('/')}/api/torrent/genDlToken?id={torrentId}"); + + var release = new ReleaseInfo + { + Guid = infoUrl, + Title = CleanTitle(torrent.Name), + Details = infoUrl, + Link = downloadUrl, + Category = MapTrackerCatToNewznab(torrent.Category), + Description = torrent.Description, + Files = int.Parse(torrent.NumFiles), + Size = long.Parse(torrent.Size), + Grabs = int.Parse(torrent.Status.TimesCompleted), + Seeders = int.Parse(torrent.Status.Seeders), + Peers = int.Parse(torrent.Status.Seeders) + int.Parse(torrent.Status.Leechers), + DownloadVolumeFactor = torrent.Status.Discount.ToUpperInvariant() switch + { + "FREE" => 0, + "_2X_FREE" => 0, + "PERCENT_50" => 0.5, + "_2X_PERCENT_50" => 0.5, + "PERCENT_70" => 0.3, + _ => 1 + }, + UploadVolumeFactor = torrent.Status.Discount.ToUpperInvariant() switch + { + "_2X_FREE" => 2, + "_2X_PERCENT_50" => 2, + _ => 1 + }, + MinimumRatio = 1, + MinimumSeedTime = 172800 // 2 days + }; + + if (torrent.Imdb.IsNotNullOrWhiteSpace()) + { + release.Imdb = ParseUtil.GetImdbId(torrent.Imdb.Split('/').LastOrDefault()).GetValueOrDefault(); + } + + if (torrent.Status?.CreatedDate != null && + DateTime.TryParseExact($"{torrent.Status.CreatedDate} +08:00", "yyyy-MM-dd HH:mm:ss zzz", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var publishDate)) + { + release.PublishDate = publishDate; + } + + releases.Add(release); + } + + return releases; + } + + private static string CleanTitle(string title) + { + title = Regex.Replace(title, @"\s+", " ", RegexOptions.Compiled); + + return title.Trim(); + } + } + + internal enum MTeamTpRequestType + { + Normal, + Adult + } + + internal class MTeamTpApiSearchQuery + { + [JsonProperty(Required = Required.Always)] + public MTeamTpRequestType Mode { get; set; } + + [JsonProperty(Required = Required.Always)] + public IEnumerable Categories { get; set; } + + public string Discount { get; set; } + public string Imdb { get; set; } + public string Keyword { get; set; } + public int? PageNumber { get; set; } + public int? PageSize { get; set; } + } + + internal class MTeamTpApiResponse + { + public MTeamTpApiData Data { get; set; } + } + + internal class MTeamTpApiData + { + [JsonPropertyName("data")] + public IReadOnlyCollection Torrents { get; set; } + } + + internal class MTeamTpApiTorrent + { + public string Id { get; set; } + public string Name { get; set; } + + [JsonPropertyName("smallDescr")] + public string Description { get; set; } + + public string Category { get; set; } + + [JsonPropertyName("numfiles")] + public string NumFiles { get; set; } + + public string Imdb { get; set; } + public string Size { get; set; } + public MTeamTpApiReleaseStatus Status { get; set; } + } + + internal class MTeamTpApiReleaseStatus + { + public string CreatedDate { get; set; } + public string Discount { get; set; } + public string TimesCompleted { get; set; } + public string Seeders { get; set; } + public string Leechers { get; set; } + } + + internal class MTeamTpApiDownloadTokenResponse + { + public string Data { get; set; } + } +} diff --git a/src/Jackett.Common/Models/IndexerConfig/Bespoke/ConfigurationDataMTeamTp.cs b/src/Jackett.Common/Models/IndexerConfig/Bespoke/ConfigurationDataMTeamTp.cs new file mode 100644 index 000000000..a14fda2df --- /dev/null +++ b/src/Jackett.Common/Models/IndexerConfig/Bespoke/ConfigurationDataMTeamTp.cs @@ -0,0 +1,17 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Jackett.Common.Models.IndexerConfig.Bespoke +{ + [ExcludeFromCodeCoverage] + internal class ConfigurationDataMTeamTp : ConfigurationData + { + public StringConfigurationItem ApiKey { get; private set; } + public BoolConfigurationItem FreeleechOnly { get; private set; } + + public ConfigurationDataMTeamTp() + { + ApiKey = new StringConfigurationItem("API Key"); + FreeleechOnly = new BoolConfigurationItem("Search freeleech only") { Value = false }; + } + } +} diff --git a/src/Jackett.Common/Serializer/STJson.cs b/src/Jackett.Common/Serializer/STJson.cs index e87acaf9f..b087264d6 100644 --- a/src/Jackett.Common/Serializer/STJson.cs +++ b/src/Jackett.Common/Serializer/STJson.cs @@ -47,5 +47,10 @@ namespace Jackett.Common.Serializer return false; } } + + public static string ToJson(object obj) + { + return JsonSerializer.Serialize(obj, _SerializerSettings); + } } } diff --git a/src/Jackett.Updater/Program.cs b/src/Jackett.Updater/Program.cs index 7707604ef..d4325c8dd 100644 --- a/src/Jackett.Updater/Program.cs +++ b/src/Jackett.Updater/Program.cs @@ -499,6 +499,8 @@ namespace Jackett.Updater "Definitions/moviesite.yml", "Definitions/movietorrent.yml", // will need c# #11284 "Definitions/moviezone.yml", // migrated to teracod #9743 + "Definitions/mteamtp.yml", // migrated to C# (API) + "Definitions/mteamtp2fa.yml", // migrated to C# (API) "Definitions/music-master.yml", "Definitions/muziekfabriek.yml", "Definitions/nachtwerk.yml",