From 6600fceb1a430d46cefaddc240b9d660ec393eba Mon Sep 17 00:00:00 2001 From: iPromKnight Date: Sat, 9 Mar 2024 14:25:06 +0000 Subject: [PATCH] Wip Blacklisting dmm porn Create adult text classifier ML Model wip - starting to write PTN in c# More work on season, show and movie parsing Remove ML project --- src/node/consumer/package.json | 1 - .../src/lib/services/metadata_service.ts | 28 +- .../services/configuration_service.test.ts | 2 + .../test/services/mongo_repository.test.ts | 32 +- src/producer/Configuration/adultcontent.json | 4678 +++++++++++++++++ .../Extensions/ConfigurationExtensions.cs | 3 +- .../Crawlers/Dmm/DebridMediaManagerCrawler.cs | 62 +- .../Crawlers/Dmm/ParsingService.Regex.cs | 29 + .../Features/Crawlers/Dmm/ParsingService.cs | 324 ++ .../Crawlers/Torrentio/TorrentioCrawler.cs | 1 - .../Features/Crawlers/Tpb/TpbCrawler.cs | 67 +- .../JobSupport/ServiceCollectionExtensions.cs | 6 +- .../ParseTorrentTitle/AudioChannels.cs | 13 + .../ParseTorrentTitle/AudioChannelsParser.cs | 50 + .../Features/ParseTorrentTitle/AudioCodecs.cs | 22 + .../ParseTorrentTitle/AudioCodecsParser.cs | 138 + .../Features/ParseTorrentTitle/BaseParsed.cs | 18 + .../Features/ParseTorrentTitle/Complete.cs | 14 + .../Features/ParseTorrentTitle/Edition.cs | 26 + .../ParseTorrentTitle/EditionParser.cs | 101 + .../ParseTorrentTitle/FileExtensionParser.cs | 78 + .../Features/ParseTorrentTitle/GroupParser.cs | 69 + .../Features/ParseTorrentTitle/Language.cs | 50 + .../ParseTorrentTitle/LanguageParser.cs | 340 ++ .../ParseTorrentTitle/ParsedFilename.cs | 11 + .../Features/ParseTorrentTitle/ParsedMovie.cs | 5 + .../Features/ParseTorrentTitle/ParsedTv.cs | 16 + .../ParseTorrentTitle/QualityModel.cs | 9 + .../ParseTorrentTitle/QualityModifier.cs | 10 + .../ParseTorrentTitle/QualityParser.cs | 230 + .../Features/ParseTorrentTitle/Resolution.cs | 13 + .../ParseTorrentTitle/ResolutionParser.cs | 53 + .../Features/ParseTorrentTitle/Revision.cs | 7 + .../Features/ParseTorrentTitle/Season.cs | 16 + .../SeasonParser.RejectRegex.cs | 44 + .../SeasonParser.ValidRegex.cs | 248 + .../ParseTorrentTitle/SeasonParser.cs | 303 ++ .../Features/ParseTorrentTitle/Source.cs | 20 + .../ParseTorrentTitle/SourceParser.cs | 151 + .../Features/ParseTorrentTitle/TitleParser.cs | 200 + .../ParseTorrentTitle/TorrentTitleParser.cs | 96 + .../Features/ParseTorrentTitle/VideoCodecs.cs | 16 + .../ParseTorrentTitle/VideoCodecsParser.cs | 89 + .../Text/AdultContentConfiguration.cs | 14 + src/producer/Features/Text/FuzzySearcher.cs | 13 + src/producer/Features/Text/IFuzzySearcher.cs | 6 + src/producer/Features/Text/SearchOptions.cs | 7 + .../Features/Text/SearchResultRecords.cs | 16 + .../Text/ServiceCollectionExtensions.cs | 26 + src/producer/GlobalUsings.cs | 7 + src/producer/Producer.csproj | 2 + src/producer/Program.cs | 1 + 52 files changed, 7699 insertions(+), 82 deletions(-) create mode 100644 src/producer/Configuration/adultcontent.json create mode 100644 src/producer/Features/Crawlers/Dmm/ParsingService.Regex.cs create mode 100644 src/producer/Features/Crawlers/Dmm/ParsingService.cs create mode 100644 src/producer/Features/ParseTorrentTitle/AudioChannels.cs create mode 100644 src/producer/Features/ParseTorrentTitle/AudioChannelsParser.cs create mode 100644 src/producer/Features/ParseTorrentTitle/AudioCodecs.cs create mode 100644 src/producer/Features/ParseTorrentTitle/AudioCodecsParser.cs create mode 100644 src/producer/Features/ParseTorrentTitle/BaseParsed.cs create mode 100644 src/producer/Features/ParseTorrentTitle/Complete.cs create mode 100644 src/producer/Features/ParseTorrentTitle/Edition.cs create mode 100644 src/producer/Features/ParseTorrentTitle/EditionParser.cs create mode 100644 src/producer/Features/ParseTorrentTitle/FileExtensionParser.cs create mode 100644 src/producer/Features/ParseTorrentTitle/GroupParser.cs create mode 100644 src/producer/Features/ParseTorrentTitle/Language.cs create mode 100644 src/producer/Features/ParseTorrentTitle/LanguageParser.cs create mode 100644 src/producer/Features/ParseTorrentTitle/ParsedFilename.cs create mode 100644 src/producer/Features/ParseTorrentTitle/ParsedMovie.cs create mode 100644 src/producer/Features/ParseTorrentTitle/ParsedTv.cs create mode 100644 src/producer/Features/ParseTorrentTitle/QualityModel.cs create mode 100644 src/producer/Features/ParseTorrentTitle/QualityModifier.cs create mode 100644 src/producer/Features/ParseTorrentTitle/QualityParser.cs create mode 100644 src/producer/Features/ParseTorrentTitle/Resolution.cs create mode 100644 src/producer/Features/ParseTorrentTitle/ResolutionParser.cs create mode 100644 src/producer/Features/ParseTorrentTitle/Revision.cs create mode 100644 src/producer/Features/ParseTorrentTitle/Season.cs create mode 100644 src/producer/Features/ParseTorrentTitle/SeasonParser.RejectRegex.cs create mode 100644 src/producer/Features/ParseTorrentTitle/SeasonParser.ValidRegex.cs create mode 100644 src/producer/Features/ParseTorrentTitle/SeasonParser.cs create mode 100644 src/producer/Features/ParseTorrentTitle/Source.cs create mode 100644 src/producer/Features/ParseTorrentTitle/SourceParser.cs create mode 100644 src/producer/Features/ParseTorrentTitle/TitleParser.cs create mode 100644 src/producer/Features/ParseTorrentTitle/TorrentTitleParser.cs create mode 100644 src/producer/Features/ParseTorrentTitle/VideoCodecs.cs create mode 100644 src/producer/Features/ParseTorrentTitle/VideoCodecsParser.cs create mode 100644 src/producer/Features/Text/AdultContentConfiguration.cs create mode 100644 src/producer/Features/Text/FuzzySearcher.cs create mode 100644 src/producer/Features/Text/IFuzzySearcher.cs create mode 100644 src/producer/Features/Text/SearchOptions.cs create mode 100644 src/producer/Features/Text/SearchResultRecords.cs create mode 100644 src/producer/Features/Text/ServiceCollectionExtensions.cs diff --git a/src/node/consumer/package.json b/src/node/consumer/package.json index 86369ae..3c07fc8 100644 --- a/src/node/consumer/package.json +++ b/src/node/consumer/package.json @@ -20,7 +20,6 @@ "bottleneck": "^2.19.5", "cache-manager": "^5.4.0", "fuse.js": "^7.0.0", - "google-sr": "^3.2.1", "inversify": "^6.0.2", "magnet-uri": "^6.2.0", "moment": "^2.30.1", diff --git a/src/node/consumer/src/lib/services/metadata_service.ts b/src/node/consumer/src/lib/services/metadata_service.ts index 2aa71f1..593e66a 100644 --- a/src/node/consumer/src/lib/services/metadata_service.ts +++ b/src/node/consumer/src/lib/services/metadata_service.ts @@ -10,7 +10,6 @@ import {IMetadataService} from "@interfaces/metadata_service"; import {IMongoRepository} from "@mongo/interfaces/mongo_repository"; import {IocTypes} from "@setup/ioc_types"; import axios from 'axios'; -import {ResultTypes, search} from 'google-sr'; import {inject, injectable} from "inversify"; import nameToImdb from 'name-to-imdb'; @@ -46,10 +45,7 @@ export class MetadataService implements IMetadataService { const name = this.escapeTitle(info.title!); const year = info.year || (info.date && info.date.slice(0, 4)); const key = `${name}_${year || 'NA'}_${info.type}`; - const query = `${name} ${year || ''} ${info.type} imdb`; - const fallbackQuery = `${name} ${info.type} imdb`; - const googleQuery = year ? query : fallbackQuery; - + const imdbInMongo = await this.mongoRepository.getImdbId(name, info.type, year); if (imdbInMongo) { @@ -62,8 +58,7 @@ export class MetadataService implements IMetadataService { ); return imdbId && 'tt' + imdbId.replace(/tt0*([1-9][0-9]*)$/, '$1').padStart(7, '0'); } catch (error) { - const imdbIdFallback = await this.getIMDbIdFromGoogle(googleQuery); - return imdbIdFallback && 'tt' + imdbIdFallback.toString().replace(/tt0*([1-9][0-9]*)$/, '$1').padStart(7, '0'); + return undefined; } } @@ -220,23 +215,4 @@ export class MetadataService implements IMetadataService { }); }); }; - - private getIMDbIdFromGoogle = async (query: string): Promise => { - try { - const searchResults = await search({query: query}); - for (const result of searchResults) { - if (result.type === ResultTypes.SearchResult) { - if (result.link.includes('imdb.com/title/')) { - const match = result.link.match(/imdb\.com\/title\/(tt\d+)/); - if (match) { - return match[1]; - } - } - } - } - return undefined; - } catch (error) { - throw new Error('Failed to find IMDb ID from Google search'); - } - }; } diff --git a/src/node/consumer/test/services/configuration_service.test.ts b/src/node/consumer/test/services/configuration_service.test.ts index 3b01aea..2e3377a 100644 --- a/src/node/consumer/test/services/configuration_service.test.ts +++ b/src/node/consumer/test/services/configuration_service.test.ts @@ -103,10 +103,12 @@ describe('Configuration Tests', () => { it('should populate metadataConfig correctly', async() => { process.env.IMDB_CONCURRENT = '1'; process.env.IMDB_INTERVAL_MS = '1000'; + process.env.TITLE_MATCH_THRESHOLD = '0.1'; const {configurationService} = await import("@services/configuration_service"); const {metadataConfig} = configurationService; expect(metadataConfig.IMDB_CONCURRENT).toBe(1); expect(metadataConfig.IMDB_INTERVAL_MS).toBe(1000); + expect(metadataConfig.TITLE_MATCH_THRESHOLD).toBe(0.1); }); it('should populate rabbitConfig correctly', async () => { diff --git a/src/node/consumer/test/services/mongo_repository.test.ts b/src/node/consumer/test/services/mongo_repository.test.ts index 771b9d5..e88bfee 100644 --- a/src/node/consumer/test/services/mongo_repository.test.ts +++ b/src/node/consumer/test/services/mongo_repository.test.ts @@ -5,20 +5,28 @@ import {MongoRepository} from "@mongo/mongo_repository"; import {IocTypes} from "@setup/ioc_types"; import {Container, inject} from "inversify"; -jest.mock('@services/configuration_service', () => { +const metadataConfig = { + TITLE_MATCH_THRESHOLD: 0.25, +} + +const cacheConfig = { + MONGODB_HOST: 'localhost', + MONGODB_PORT: '27017', + MONGODB_DB: 'knightcrawler', + MONGODB_USER: 'mongo', + MONGODB_PASSWORD: 'mongo', + get MONGO_URI(): string { + return `mongodb://${this.MONGODB_USER}:${this.MONGODB_PASSWORD}@${this.MONGODB_HOST}:${this.MONGODB_PORT}/${this.MONGODB_DB}?authSource=admin`; + }, +}; + +jest.doMock('@services/configuration_service', () => { return { configurationService: { - cacheConfig: { - MONGODB_HOST: 'localhost', - MONGODB_PORT: '27017', - MONGODB_DB: 'knightcrawler', - MONGODB_USER: 'mongo', - MONGODB_PASSWORD: 'mongo', - get MONGO_URI(): string { - return `mongodb://${this.MONGODB_USER}:${this.MONGODB_PASSWORD}@${this.MONGODB_HOST}:${this.MONGODB_PORT}/${this.MONGODB_DB}?authSource=admin`; - } - }, - } + cacheConfig: cacheConfig, + metadataConfig: metadataConfig, + }, + } }); diff --git a/src/producer/Configuration/adultcontent.json b/src/producer/Configuration/adultcontent.json new file mode 100644 index 0000000..7cb5ffa --- /dev/null +++ b/src/producer/Configuration/adultcontent.json @@ -0,0 +1,4678 @@ +{ + "AdultContentSettings": { + "Allow": false, + "Threshold": 85, + "Keywords": [ + "1000facials", + "10musume", + "1111customsxxx", + "18eighteen", + "18tokyo", + "18vr", + "1by-day", + "1passforallsites", + "1pondo", + "21naturals", + "21roles", + "21sextreme", + "21sextury", + "2girls1camera", + "40somethingmag", + "50plusmilfs", + "5kporn", + "5kteens", + "60plusmilfs", + "65inchhugeasses", + "abbiemaley", + "abbywinters", + "abuseme", + "accidentalgangbang", + "activeduty", + "adamandevepictures", + "addicted2girls", + "addisonstreet", + "adultanime", + "adultdvdempire", + "adultdvdmarketplace", + "adultempire", + "adultfilmindex", + "adultprime", + "adulttime", + "adulttimepilots", + "aebn", + "anal", + "agentredgirl", + "alettaoceanempire", + "alexismonroe", + "alexlegend", + "aliciasgoddesses", + "allanal", + "allanalallthetime", + "allblackx", + "allfinegirls", + "allgirlmassage", + "allherluv", + "allinternal", + "alljapanesepass", + "allvr", + "alphamales", + "alsscan", + "amateripremium", + "amateurallure", + "amateurav", + "amateurboxxx", + "amateure-xtreme", + "amateureuro", + "amateursfrombohemia", + "amazinganna", + "ambushmassage", + "amelielou", + "americanmusclehunks", + "amkingdom", + "amourangels", + "anal-angels", + "anal-beauty", + "anal4k", + "analacrobats", + "analamateur", + "analbbc", + "analcheckups", + "analhookers", + "analized", + "analjust", + "analmom", + "analnippon", + "analonly", + "analoverdose", + "analteenangels", + "analtherapyxxx", + "analvids", + "analviolation", + "analyzedgirls", + "andolinixxl", + "angelasommers", + "angelawhite", + "angelinacastrolive", + "anidb", + "anilos", + "animecharactersdatabase", + "antoniosuleiman", + "apovstory", + "archangelvideo", + "ariellynn", + "ashemaletube", + "ashleyfires", + "ashlynnbrooke", + "asian18", + "asianamericantgirls", + "asianfever", + "asiansexdiary", + "asiantgirl", + "asmrfantasy", + "assholefever", + "assmeat", + "assteenmouth", + "asstraffic", + "assumethepositionstudios", + "assylum", + "atkexotics", + "atkgirlfriends", + "atkhairy", + "atkpetites", + "atkpremium", + "attackboys", + "auntjudys", + "auntjudysxxx", + "auntykathy", + "aussieass", + "aussiefellatioqueens", + "aussiepov", + "austinwilde", + "av69", + "avadawn", + "avanal", + "aventertainments", + "avidolz", + "avjiali", + "avstockings", + "avtits", + "aziani", + "babearchives", + "babepedia", + "baberotica", + "baberoticavr", + "babes", + "babesandstars", + "babesnetwork", + "babevr", + "backdoorpov", + "backroomcastingcouch", + "baddaddypov", + "badfamilypov", + "badmilfs", + "badmommypov", + "badoinkvr", + "badteenspunished", + "baeb", + "balletflatsfetish", + "bamvisions", + "bangbangboys", + "bangbros", + "bangingbeauties", + "bangteenpussy", + "barbarafeet", + "barebackplus", + "barelylegal", + "baretwinks", + "bathhousebait", + "battlebang", + "bbcparadise", + "bbcpie", + "bbcsurprise", + "beauty-angels", + "beauty4k", + "beaverhunt", + "becomingfemme", + "behindtrans500", + "beingphoenixmarie", + "belamionline", + "bellahd", + "bellanextdoor", + "bellapass", + "bellesafilms", + "bellesahouse", + "beltbound", + "berryboys", + "bestoftealconrad", + "bffs", + "bigbootytgirls", + "bigfatcreampie", + "biggulpgirls", + "bigtitstokyo", + "biguysfuck", + "billiestar", + "biphoria", + "bjraw", + "black-tgirls", + "black4k", + "blackambush", + "blackandbig", + "blackboyaddictionz", + "blacked", + "blackedraw", + "Blackfoxbound UK", + "blackmarketxxx", + "blackmassivecocks", + "blackmeatwhitefeet", + "blackph", + "blacksonblondes", + "blacksoncougars", + "blacksondaddies", + "blacktgirlshardcore", + "blackvalleygirls", + "blackwhitefuckfest", + "blakemason", + "blowmepov", + "blownbyrone", + "blowpass", + "bobbiedenlive", + "bobstgirls", + "bolatino", + "bondagecafe", + "bondageliberation", + "boobpedia", + "bootyclapxxx", + "bootysisters", + "boppingbabes", + "bossymilfs", + "bouncychicks", + "boundheat", + "boundhoneys", + "boundjocks", + "boundlife", + "boundtwinks", + "bountyhunterporn", + "boxtrucksex", + "boycrush", + "boyforsale", + "boyfriendsharing", + "boyfun", + "boygusher", + "boysdestroyed", + "boysfuckmilfs", + "boyshalfwayhouse", + "bradmontana", + "brandibelle", + "brandnewamateurs", + "brandnewfaces", + "brasilvr", + "brattyfamily", + "brattymilf", + "brattysis", + "bravofucker", + "brazilian-transsexuals", + "braziltgirls", + "brazzers", + "breeditraw", + "brett-tyler", + "brickyates", + "bride4k", + "brokenlatinawhores", + "brokensluts", + "brokestraightboys", + "brookelynnebriar", + "bruceandmorgan", + "brutalinvasion", + "bryci", + "bskow", + "bukkakenow", + "bulldogxxx", + "burningangel", + "bushybushy", + "bustybeauties", + "buttman", + "cadinot", + "calicarter", + "camwhores", + "canada-tgirl", + "caribbeancom", + "caribbeancompr", + "carmenvalentina", + "carnalplus", + "castingcouch-x", + "catalinacruz", + "cazzofilm", + "cfnmeu", + "chaosmen", + "charleechaselive", + "chastitybabes", + "cheatingsis", + "cherrypimps", + "chickpass", + "chickpassinternational", + "chickpasspornstars", + "chickpassteens", + "chloelamour", + "chocolatebjs", + "citebeur", + "clairprod", + "class-lesbians", + "claudiamarie", + "clips4sale", + "clubdom", + "clubelegantangel", + "clubinfernodungeon", + "clubseventeen", + "clubsweethearts", + "clubtug", + "cockhero", + "cocksuremen", + "cockyboys", + "codycummings", + "colbyknox", + "collectivecorruption", + "college-amateurs", + "college-uniform", + "collegeboyphysicals", + "collegedudes", + "collegefuckparties", + "coltstudiogroup", + "combatzone", + "combatzonexxx", + "concoxxxion", + "corbinfisher", + "cosplayfeet", + "cospuri", + "cougarseason", + "crashpadseries", + "creampie-angels", + "creativeporn", + "cruelgf", + "crunchboy", + "cuck4k", + "cuckhunter", + "cuckoldsessions", + "culioneros", + "cum4k", + "cumaholicteens", + "cumbang", + "cumblastcity", + "cumdumpsterteens", + "cumforcover", + "cumlouder", + "cumshotoasis", + "cumswappingsis", + "currycreampie", + "czechamateurs", + "czechanalsex", + "czechbangbus", + "czechbiporn", + "czechbitch", + "czechboobs", + "czechcasting", + "czechcouples", + "czechdeviant", + "czechdungeon", + "czechescortgirls", + "czechestrogenolit", + "czechexecutor", + "czechfantasy", + "czechfirstvideo", + "czechgame", + "czechgangbang", + "czechgardenparty", + "czechgayamateurs", + "czechgaycasting", + "czechgaycity", + "czechgaycouples", + "czechgayfantasy", + "czechgaymassage", + "czechgaysolarium", + "czechgaytoilets", + "czechgypsies", + "czechharem", + "czechhitchhikers", + "czechhomeorgy", + "czechlesbians", + "czechmassage", + "czechmegaswingers", + "czechorgasm", + "czechparties", + "czechrealdolls", + "czechsexcasting", + "czechsexparty", + "czechshemale", + "czechsolarium", + "czechspy", + "czechstreets", + "czechsupermodels", + "czechtaxi", + "czechvr", + "czechvrcasting", + "czechvrfetish", + "czechvrnetwork", + "czechwifeswap", + "d52q", + "dadcrush", + "daddy4k", + "daddycarl", + "daddygetslucky", + "daddyslilangel", + "damnthatsbig", + "danejones", + "danidaniels", + "danni", + "darkcruising", + "darkroomvr", + "darksodomy", + "darkx", + "darttechstudios", + "data18", + "datingmystepson", + "daughterjoi", + "daughterswap", + "ddfbusty", + "deauxmalive", + "debt4k", + "deeplush", + "deepthroatsirens", + "defiled18", + "dellaitwins", + "delphinefilms", + "desperateamateurs", + "detentiongirls", + "deviante", + "devianthardcore", + "devilsfilm", + "devilsfilmparodies", + "devilsgangbangs", + "devilstgirls", + "devonlee", + "dfbnetwork", + "dfxtra", + "diabolic", + "dianafeet", + "dickdrainers", + "dickontrip", + "digitaldesire", + "digitalplayground", + "dillionation", + "dirty-coach", + "dirty-doctor", + "dirtyauditions", + "dirtyboysociety", + "dirtycosplay", + "dirtyflix", + "disruptivefilms", + "dlsite", + "doegirls", + "dogfartnetwork", + "doghousedigital", + "dollrotic", + "dollsporn", + "domai", + "dorcelclub", + "dorcelvision", + "dothewife", + "doubleteamedteens", + "downblousejerk", + "downtofuckdating", + "dpfanatics", + "dreamsofspanking", + "dreamteenshd", + "dreamtranny", + "drilledchicks", + "driverxxx", + "dtfsluts", + "dyke4k", + "dyked", + "dylanryder", + "eastboys", + "ebonytugs", + "edwardjames", + "elegantangel", + "elitebabes", + "englishlads", + "enzorimenez", + "eporner", + "ericjohnssexadventures", + "ericvideos", + "erito", + "eroprofile", + "eroticax", + "eroticbeauty", + "eroticspice", + "erotiquetvlive", + "errotica-archives", + "eternaldesire", + "euro-tgirls", + "eurocreme", + "eurogirlsongirls", + "euroteenerotica", + "evilangel", + "evilangel", + "evolvedfights", + "evolvedfightslez", + "exotic4k", + "explicite-art", + "exploitedcollegegirls", + "excogigirls", + "extrapackage", + "extremepickups", + "exxxtrasmall", + "fabsluts", + "facials4k", + "facialsforever", + "fakehostel", + "fakehub", + "fakeshooting", + "faketaxi", + "falconstudios", + "fallinlovia", + "famedigital", + "familycreep", + "familyhookups", + "familylust", + "familysexmassage", + "familysinners", + "familystrokes", + "familyswap", + "familytherapyxxx", + "familyxxx", + "fantasyflipflop", + "fantasyhd", + "fantasymassage", + "faphouse", + "feetishpov", + "femanic", + "femdomempire", + "feminized", + "femjoy", + "femlatex", + "femout", + "femoutsex", + "ferame", + "fetishnetwork", + "fetishpros", + "ffstockings", + "filf", + "fillyfilms", + "filthflix", + "filthyfamily", + "filthygapers", + "filthykings", + "finishesthejob", + "finishhim", + "firstanalquest", + "firstbgg", + "firstclasspov", + "fist4k", + "fistertwister", + "fistflush", + "fistflush", + "fistingcentral", + "fit18", + "fitting-room", + "footfetishbeauties", + "footfetishdaily", + "footsiebabes", + "forbiddenfruitsfilms", + "forbiddenseductions", + "forbondage", + "forgivemefather", + "fostertapes", + "fourfingerclub", + "foxxedup", + "fragileslave", + "franks-tgirlworld", + "fratx", + "freakmobmedia", + "freeones", + "freeones", + "freeusefantasy", + "freeusemilf", + "french-twinks", + "frenchporn", + "freshoutofhighschool", + "frolicme", + "ftmmen", + "ftmplus", + "fuckedfeet", + "fuckedhard18", + "fuckermate", + "fuckfiesta", + "fuckingawesome", + "fuckinghardcore", + "fuckingoffice", + "fuckingparties", + "fuckingstreet", + "fuckstudies", + "fuckthegeek", + "fullpornnetwork", + "funbags", + "funsizeboys", + "futanari", + "futanarica", + "gag-n-gape", + "gangav", + "gangbangcreampie", + "gapingangels", + "gayarabclub", + "gaycastings", + "gaycest", + "gaycreeps", + "gaydvdempire", + "gayempire", + "gayfrenchkiss", + "gayhoopla", + "gayhorrorporn", + "gayroom", + "gayviolations", + "genderxfilms", + "genlez", + "girlcum", + "girlfaction", + "girlfriendsfilms", + "girlgirl", + "girlgirlmania", + "girlgirlxxx", + "girlgrind", + "girlsandstuds", + "girlsgotcream", + "girlsonlyporn", + "girlsoutwest", + "girlsrimming", + "girlstakeaway", + "girlstryanal", + "girlsunderarrest", + "girlsway", + "girlswhofuckgirls", + "givemepink", + "givemeteens", + "gloryhole", + "gloryholeinitiations", + "gloryholesecrets", + "gloryholeswallow", + "glosstightsglamour", + "goddessnudes", + "goddesssnow", + "goldenslut", + "gostuckyourself", + "gotfilled", + "grannyghetto", + "grannyvsbbc", + "grooby-archives", + "grooby", + "groobygirls", + "groobyvr", + "guysinsweatpants", + "gymnastic", + "gymrotic", + "gynoexclusive", + "hairyav", + "hairyundies", + "handdomination", + "handsonhardcore", + "hanime", + "hardcoreyouth", + "hardfuckgirls", + "hardkinks", + "hardonvr", + "hardtied", + "hardx", + "harlemsex", + "harmonyvision", + "hazel-tucker", + "hd19", + "hdmassageporn", + "hdsex18", + "heavyonhotties", + "hegre", + "helixstudios", + "helloladyboy", + "hentai2read", + "hentaied", + "hergape", + "hersexdebut", + "heymilf", + "heyoutdoor", + "heyzo", + "hijabhookup", + "hijabmylfs", + "himeros", + "hitzefrei", + "hmvmania", + "hobybuchanon", + "holed", + "hollyrandall", + "homeclips", + "homemadeanalwhores", + "hometowngirls", + "hometownhoneys", + "honeytrans", + "hongkongslut", + "hookuphotshot", + "hornydoctor", + "hornygirlscz", + "hornyhousehold", + "horrorporn", + "hotandtatted", + "hotcast", + "hotcrazymess", + "hotguysfuck", + "hothouse", + "hotlegsandfeet", + "hotmilfsfuck", + "hotmovies", + "hotoldermale", + "hottiemoms", + "hotwifexxx", + "houseofgord", + "houseoftaboo", + "houseofyre", + "hqporner", + "hucows", + "hugecockbreak", + "hungarianhoneys", + "hungfuckers", + "hunt4k", + "hunterleigh", + "hunterpov", + "hushpass", + "hussiepass", + "hustlaz", + "hustler", + "hustlerhd", + "hustlerparodies", + "hustlerslesbians", + "hustlerstaboo", + "hypnotube", + "iafd", + "iconmale", + "idols69", + "ifeelmyself", + "ignore4k", + "ihuntmycunt", + "ikillitts", + "ikissgirls", + "iknowthatgirl", + "immorallive", + "imnotyourmommy", + "infernalrestraints", + "innocenthigh", + "insex", + "insexondemand", + "interracialblowbang", + "interracialpass", + "interracialpickups", + "interracialpovs", + "intimatelesbians", + "intimatepov", + "ipinkvisualpass", + "isthisreal", + "italianshotclub", + "itscleolive", + "itspov", + "iwantclips", + "iwara", + "jacquieetmichelelite", + "jacquieetmicheltv", + "jalifstudio", + "jamesdeen", + "janafox", + "japaneseflashers", + "japaneseslurp", + "japanhdv", + "japanlust", + "japornxxx", + "jasonsparkslive", + "jav", + "javdb", + "javdb36", + "javhd", + "javhub", + "javlibrary", + "javlibrary", + "jaysinxxx", + "jayspov", + "jbvideo", + "jcosplay", + "jeedoo", + "jeffsmodels", + "jelenajensen", + "jerk-offpass", + "jerkaoke", + "jessicajaymesxxx", + "jessroyan", + "jimweathersarchives", + "jizzbomb", + "jnrc", + "jockbreeders", + "jockpussy", + "jodiwest", + "joeperv", + "johnnyrapid", + "joibabes", + "jonnidarkkoxxx", + "joybear", + "joymii", + "jpmilfs", + "jpnurse", + "jpshavers", + "jpteacher", + "jschoolgirls", + "julesjordan", + "juliaannlive", + "karissa-diamond", + "karups", + "katiebanks", + "kellymadison", + "kendrajames", + "killergram", + "kimberleelive", + "kin8tengoku", + "kingsoffetish", + "kink305", + "kinkbomb", + "kinkvr", + "kinkyfamily", + "kinkymistresses", + "kinkyspa", + "kinkytwink", + "kissmefuckme", + "kpopping", + "kristenbjorn", + "ladyboy-ladyboy", + "ladyboy", + "ladyboygold", + "ladydee", + "lanakendrick", + "lanesisters", + "lasublimexxx", + "latinamilf", + "latinoguysporn", + "leannecrow", + "legsex", + "lesbea", + "lesbiananalsluts", + "lesbianass", + "lesbianfactor", + "lesbiantribe", + "lesbianx", + "lesworship", + "lethalhardcore", + "lethalhardcorevr", + "lethalpass", + "letsdoeit", + "lewood", + "lewrubens", + "lexidona", + "lexingtonsteele", + "lezcuties", + "lifeselector", + "lilhumpers", + "lingerieav", + "lingerietales", + "littleasians", + "littlecaprice-dreams", + "littlefromasia", + "littlehellcat", + "loan4k", + "lonestarangel", + "lookathernow", + "lordaardvark", + "lovehairy", + "loveherass", + "loveherboobs", + "loveherfeet", + "loveherfilms", + "lubed", + "lucasentertainment", + "lustcinema", + "lustery", + "lustreality", + "lustylina", + "mackstudio", + "madeincanada", + "madouqu", + "maggiegreenlive", + "maketeengape", + "maledigital", + "malefeet4u", + "mamacitaz", + "mandyflores", + "manojob", + "manroyale", + "manuelferrara", + "manyvids", + "marcusmojo", + "mariskax", + "maskurbate", + "masonicboys", + "masqulin", + "massage-parlor", + "massagebait", + "mature4k", + "maturegapers", + "maturegynoexam", + "maturegynospy", + "max-hardcore", + "maxence-angel", + "maxinex", + "meanawolf", + "meanbitches", + "meanmassage", + "meetsuckandfuck", + "menatplay", + "menoboy", + "menover30", + "menpov", + "messyxxx", + "metadataapi", + "metadataapi", + "metadataapi", + "metalbondage", + "metart", + "metartnetwork", + "metartx", + "milehighmedia", + "milfed", + "milfsodomy", + "milfthing", + "milftrip", + "milftugs", + "milfvr", + "milkingtable", + "milovana", + "minimuff", + "minnano-av", + "missax", + "mistermale", + "mmpnetwork", + "modelcentro", + "modelhub", + "modelmediaasia", + "modelmediaus", + "modeltime", + "moderndaysins", + "mofos", + "mofosnetwork", + "mom4k", + "momcomesfirst", + "momcum", + "momisamilf", + "momlover", + "mommy4k", + "mommyblowsbest", + "mommyjoi", + "mommysboy", + "mommysboy", + "mommysgirl", + "momsbangteens", + "momsboytoy", + "momsfamilysecrets", + "momslickteens", + "momsteachsex", + "momstight", + "momswap", + "momswapped", + "momwantscreampie", + "momwantstobreed", + "momxxx", + "mongerinasia", + "monicamendez", + "monroelee", + "monstercub", + "mormongirlz", + "motherfuckerxxx", + "motherless", + "movieporn", + "mplstudios", + "mrbigfatdick", + "mrluckypov", + "mrpov", + "muchaslatinas", + "mugfucked", + "mugfucked", + "muses", + "my-slavegirl", + "mybabysittersclub", + "mybadmilfs", + "mycherrycrush", + "mydaughterswap", + "mydirtyhobby", + "myfamilypies", + "myfirstdaddy", + "mylf", + "mylfdom", + "mylifeinmiami", + "mylked", + "mypervmom", + "mypervyfamily", + "myracequeens", + "mysislovesme", + "mysweetapple", + "myteenoasis", + "myveryfirsttime", + "n53i", + "nakedsword", + "nannyspy", + "nastypublicsex", + "nastystepfamily", + "nataliastarr", + "natashanice", + "naturalbornbreeders", + "naughtyamerica", + "naughtyamericavr", + "naughtynatural", + "netvideogirls", + "newgrounds", + "newsensations", + "newsensations", + "nextdoorbuddies", + "nextdoorcasting", + "nextdoorebony", + "nextdoorfilms", + "nextdoorhomemade", + "nextdoormale", + "nextdoororiginals", + "nextdoorraw", + "nextdoorstudios", + "nextdoortaboo", + "nextdoortwink", + "nfbusty", + "nhentai", + "nikkibenz", + "nikkiphoenixxx", + "ninakayy", + "noboring", + "noelalejandrofilms", + "noirmale", + "noodledude", + "notmygrandpa", + "nubilefilms", + "nubiles-casting", + "nubiles-porn", + "nubiles", + "nubileset", + "nubilesporn", + "nubilesunscripted", + "nucosplay", + "nudefightclub", + "nudeyogaporn", + "nurumassage", + "nylonfeetlove", + "nylonspunkjunkies", + "nylonsweeties", + "nylonup", + "nympho", + "ocreampies", + "officecock", + "officemsconduct", + "officepov", + "officesexjp", + "ohmyholes", + "old-n-young", + "old4k", + "older4me", + "oldgoesyoung", + "oldje-3some", + "oldje", + "oldnanny", + "oldsfuckdolls", + "only3xgirls", + "only3xlost", + "only3xseries", + "only3xvr", + "onlyblowjob", + "onlygolddigger", + "onlyprince", + "onlytarts", + "onlyteenblowjobs", + "oopsie", + "openlife", + "oraloverdose", + "oreno3d", + "orgytrain", + "outdoorjp", + "outhim", + "outofthefamily", + "over40handjobs", + "p54u", + "pacopacomama", + "paintoy", + "pandafuck", + "pansexualx", + "pantyjobs", + "pantypops", + "paradisegfs", + "parasited", + "parodypass", + "passion-hd", + "passionxxx", + "paulomassaxxx", + "pawged", + "peeonher", + "pegasproductions", + "pennypaxlive", + "penthousegold", + "perfectgirlfriend", + "perfectgonzo", + "pervcity", + "pervdoctor", + "perversefamily", + "pervertgallery", + "pervmom", + "pervnana", + "pervpricipal", + "pervtherapy", + "peternorth", + "petite18", + "petiteballerinasfucked", + "petited", + "petitehdporn", + "petiteleeanna", + "petitepov", + "philavise", + "philippwants", + "pickupfuck", + "pie4k", + "pinklabel", + "pinkoclub", + "pinkotgirls", + "pinupfiles", + "pissplay", + "pissynetwork", + "pjgirls", + "pkfstudios", + "playboy", + "playboyplus", + "playdaddy", + "playwithrae", + "plumperpass", + "plushies", + "pmvhaven", + "porkvendors", + "pornbox", + "porncornvr", + "porncz", + "pornditos", + "porndudecasting", + "pornfidelity", + "pornforce", + "porngoespro", + "pornhex", + "pornhub", + "pornhubpremium", + "pornlandvideos", + "pornmegaload", + "pornperverts", + "pornpros", + "pornstarbts", + "pornstarhardcore", + "pornstarplatinum", + "pornstarstroker", + "pornstartease", + "pornweekends", + "pornworld", + "portagloryhole", + "poundedpetite", + "povadventure", + "povav", + "povbitch", + "povblowjobs", + "povd", + "povjp", + "povmania", + "povmasters", + "povperverts", + "povpornstars", + "povr", + "povthis", + "prettydirty", + "prettydirtyteens", + "pridestudios", + "primecups", + "princesscum", + "private", + "privatecastings", + "privatesextapes", + "producersfun", + "profiles", + "propertysex", + "publicagent", + "publicfrombohemia", + "publicsexadventures", + "publicsexdate", + "puffynetwork", + "pumaswedexxx", + "pure-bbw", + "pure-ts", + "pure-xxx", + "purebj", + "puremature", + "purepov", + "puretaboo", + "pussyav", + "putalocura", + "r18", + "rachel-steele", + "rachelaldana", + "rachelstormsxxx", + "ragingstallion", + "randyblue", + "raunchybastards", + "ravenswallowzxxx", + "rawattack", + "rawcouples", + "rawfuck", + "rawfuckboys", + "reaganfoxx", + "realbikinigirls", + "realfuckingdating", + "realityjunkies", + "realitykings", + "realitylovers", + "realjamvr", + "realsensual", + "realtgirls", + "realtimebondage", + "realvr", + "reddit", + "redgifs", + "redheadmariah", + "redhotstraightboys", + "redpolishfeet", + "reidmylips", + "reidmylips", + "renderfiend", + "restrictedsenses", + "retroporncz", + "rickysroom", + "ridleydovarez", + "riggsfilms", + "rim4k", + "roccosiffredi", + "roddaily", + "rodsroom", + "romemajor", + "rubberdoll", + "rule34video", + "russian-tgirls", + "rylskyart", + "sabiendemonia", + "samanthagrace", + "samuelotoole", + "sapphicerotica", + "sapphix", + "sarajay", + "sayuncle", + "scarybigdicks", + "schoolgirlshd", + "schoolpov", + "scoreland", + "scoreland2", + "scoutboys", + "screwmetoo", + "seancody", + "seductive18", + "seehimfuck", + "seehimsolo", + "seemomsuck", + "seemysextapes", + "selfiesuck", + "sensualpain", + "serve4k", + "severesexfilms", + "sexart", + "sexbabesvr", + "sexintaxi", + "sexlikereal", + "sexmex", + "sexmywife", + "sexsee", + "sexselector", + "sexuallybroken", + "sexvr", + "sexwithmuslims", + "sexworking", + "sexyhub", + "shagmag", + "shame4k", + "shandafay", + "shanedieselsbanginbabes", + "share", + "shefucksonthefirstdate", + "shegotsix", + "shelovesblack", + "shesbrandnew", + "sheseducedme", + "shewillcheat", + "shinybound", + "shinysboundsluts", + "shiofuky", + "shoplyfter", + "shoplyftermylf", + "showerbait", + "showybeauty", + "shylaj", + "sidechick", + "silverstonedvd", + "silviasaint", + "simplyanal", + "sinematica", + "sinslife", + "siripornstar", + "sislovesme", + "sisswap", + "sissypov", + "sketboy", + "slayed", + "slr", + "slroriginals", + "slutinspection", + "slutsbts", + "slutspov", + "sluttybbws", + "smashed", + "smashpictures", + "smokingmina", + "smutfactor", + "smutmerchants", + "soapymassage", + "sofiemariexxx", + "sologirlsmania", + "soloteengirls", + "sophiedeelive", + "sororitysluts", + "spankbang", + "spankmonster", + "spearteenpussy", + "spermantino", + "spermmania", + "spermswallowers", + "spermswap", + "spizoo", + "spoiledvirgins", + "spunkworthy", + "spyfam", + "squirtalicious", + "squirted", + "squirtinglesbian", + "squirtingorgies", + "stagcollective", + "staghomme", + "stasyq", + "stasyqvr", + "staxus", + "stayhomepov", + "stephousexxx", + "steppov", + "stepsiblings", + "stepsiblingscaught", + "stockingvideos", + "stockydudes", + "str8hell", + "strapattackers", + "straplez", + "straponcum", + "strapondreamer", + "streaming", + "stretchedoutsnatch", + "stripzvr", + "strokies", + "stuck4k", + "studiofow", + "stuffintwats", + "stunning18", + "subbyhubby", + "submissivex", + "subspaceland", + "suckmevr", + "sugarcookie", + "sugardaddyporn", + "suggabunny", + "sunnylanelive", + "sunnyleone", + "superbemodels", + "superramon", + "susanayn", + "swallowbay", + "swallowed", + "swallowsalon", + "sweetcarla", + "sweetfemdom", + "sweetheartvideo", + "sweetsinner", + "sweetyx", + "swinger-blog", + "swnude", + "tabooheat", + "taboopov", + "tacamateurs", + "tadpolexstudio", + "takevan", + "taliashepard", + "tamedteens", + "tandaamateurs", + "tandaasians", + "tandablondes", + "tandabrunettes", + "tandaebony", + "tandahousewives", + "tandalatinas", + "tandalesbians", + "tandaredheads", + "tanyatate", + "taratainton", + "teacherfucksteens", + "teachmyass", + "teamskeet", + "teasepov", + "teasingandpleasing", + "teenageanalsluts", + "teenagecorruption", + "teenagetryouts", + "teenanalcasting", + "teencoreclub", + "teencorezine", + "teencurves", + "teendrillers", + "teenerotica", + "teenfidelity", + "teenfrombohemia", + "teenmegaworld", + "teenpies", + "teensandtwinks", + "teensexmania", + "teensexmovs", + "teensgoporn", + "teensloveanal", + "teensloveblackcocks", + "teenslovehugecocks", + "teensnaturalway", + "teensneaks", + "teenstryblacks", + "teenthais", + "teentugs", + "teenytaboo", + "tenshigao", + "terapatrick", + "tessafowler", + "texasbukkake", + "tgirl40", + "tgirlbbw", + "tgirljapan", + "tgirljapanhardcore", + "tgirlpornstar", + "tgirlpostop", + "tgirlsex", + "tgirlsfuck", + "tgirlshookup", + "tgirltops", + "thatsitcomshow", + "theartporn", + "theassfactory", + "thedicksuckers", + "theflourishamateurs", + "theflourishfetish", + "theflourishpov", + "theflourishxxx", + "thehabibshow", + "thelesbianexperience", + "thelifeerotic", + "thenude", + "thetabutales", + "theyeslist", + "thicc18", + "thickandbig", + "thickumz", + "thirdsexxxx", + "thirdworldxxx", + "thisvid", + "throated", + "timtales", + "tiny4k", + "tinysis", + "tittycreampies", + "titworld", + "tmwpov", + "tmwvrnet", + "tokyo-hot", + "tokyobang", + "tommydxxx", + "tonightsgirlfriend", + "toomanytrannies", + "topgrl", + "toplatindaddies", + "topwebmodels", + "toticos", + "touchmywife", + "toughlovex", + "trans500", + "trans500", + "trans500", + "transangels", + "transatplay", + "transbella", + "transcest", + "transerotica", + "transexdomination", + "transexpov", + "transfeet", + "transfixed", + "transgasm", + "transgressivefilms", + "transgressivexxx", + "transmodeldatabase", + "transnificent", + "transroommates", + "transsensual", + "transsexualangel", + "transsexualroadtrip", + "tranzvr", + "traxxx", + "trickymasseur", + "trickyoldteacher", + "trickyspa", + "trikepatrol", + "tripforfuck", + "trueamateurs", + "trueanal", + "truelesbian", + "trystanbull", + "tryteens", + "ts-castingcouch", + "tsfactor", + "tsgirlfriendexperience", + "tsplayground", + "tspov", + "tsraw", + "tsvirtuallovers", + "tugpass", + "tuktukpatrol", + "tushy", + "tushyraw", + "tutor4k", + "twinkloads", + "twinks", + "twinktop", + "twistedvisual", + "twistys", + "twistysnetwork", + "twotgirls", + "uk-tgirls", + "ultrafilms", + "underhentai", + "universblack", + "unlimitedmilfs", + "unrealporn", + "upherasshole", + "upskirtjerk", + "valentina", + "vangoren", + "vcaxxx", + "velvetveronica", + "vickyathome", + "viktor-rom", + "vinaskyxxx", + "vintagegaymovies", + "vip4k", + "vipissy", + "vipsexvault", + "virtualpee", + "virtualporn", + "virtualrealamateurporn", + "virtualrealgay", + "virtualrealjapan", + "virtualrealpassion", + "virtualrealporn", + "virtualrealtrans", + "virtualtaboo", + "visit-x", + "vivid", + "vividclassic", + "vivthomas", + "vlogxxx", + "vogov", + "vrbangers", + "vrbgay", + "vrbtrans", + "vrconk", + "vrcosplayx", + "vrfirsttimer", + "vrhush", + "vrintimacy", + "vrlatina", + "vrporn", + "vrporncz", + "vrpornpass", + "vrxdb", + "vtubie", + "wakeupnfuck", + "wankitnow", + "wankz", + "wankzvr", + "warashi-asian-pornstars", + "watch4beauty", + "watch4fetish", + "watchingmydaughtergoblack", + "watchingmymomgoblack", + "watchmygf", + "watchreal", + "watchyoujerk", + "waybig", + "wcpclub", + "wearehairy", + "webyoung", + "wefuckblackgirls", + "wefuckblackgirls", + "welikegirls", + "weliketosuck", + "welivetogether", + "weneednewtalents", + "westcoastproductions", + "wetandpissy", + "wetandpuffy", + "wetvr", + "whiteghetto", + "whiteteensblackcocks", + "whorecraftvr", + "wifespov", + "wildoncam", + "williamhiggins", + "willtilexxx", + "wolfwagner", + "woodmancastingx", + "wowgirls", + "wowgirlsblog", + "wowporn", + "wtfpass", + "wurstfilm", + "x-angels", + "x-art", + "xart", + "xcoreclub", + "xempire", + "xes", + "xevunleashed", + "xhamster", + "xlgirls", + "xnxx", + "xrares", + "xsinsvr", + "xslist", + "xtube", + "xvideos", + "xvideos", + "xvirtual", + "xxxjobinterviews", + "xxxnj", + "xxxpawn", + "yanks", + "yesgirlz", + "yngr", + "younganaltryouts", + "youngerloverofmine", + "youngermommy", + "youngthroats", + "youporn", + "yourmomdoesanal", + "yourmomdoesporn", + "yummysofie", + "z-filmz-originals", + "zebragirls", + "zentaidolls", + "zerotolerancefilms", + "zexyvr", + "zishy", + "zoiestarr", + "abbey brooks", + "accidental anal", + "accidental creampie", + "accidental orgasm", + "addison rose", + "adriana nicole", + "adriana sage", + "adrianna nicole", + "adult porn", + "adult tube", + "adult tubes", + "adult you tube", + "adult youtube", + "african anal", + "african porn", + "aladdin porn", + "alanah rae", + "alaura eden", + "albino porn", + "alektra blue", + "aletta ocean", + "alexa may", + "alexa rae", + "alexis amor", + "alexis amore", + "alexis breeze", + "alexis love", + "alexis silver", + "alexis texas", + "alice in wonderland porn", + "alicia angel", + "alicia rhodes", + "alicia tyler", + "alien porn", + "allie sin", + "allysin chaynes", + "amateur allure", + "amateur anal", + "amateur blowjob", + "amateur bukkake", + "amateur couple", + "amateur creampie", + "amateur cuckold", + "amateur cumshots", + "amateur gangbang", + "amateur handjob", + "amateur lesbian", + "amateur milf", + "amateur orgasm", + "amateur orgy", + "amateur porn", + "amateur porn tube", + "amateur porno", + "amateur sex", + "amateur sex videos", + "amateur swingers", + "amateur teen", + "amateur threesome", + "amateur tube", + "amateur wife", + "amateurs gone wild", + "amature sex", + "amazing ass", + "amazing blowjob", + "amazing deepthroat", + "amazing orgasm", + "amazing porn", + "amazing tits", + "amazon porn", + "amber lynn", + "amber michaels", + "amber rayne", + "american porn", + "american pussy", + "amputee sex", + "amy reid", + "ana nova", + "anal accident", + "anal amateur", + "anal asian", + "anal beads", + "anal blonde", + "anal bondage", + "anal bukkake", + "anal casting", + "anal compilation", + "anal cream", + "anal creampie", + "anal creampie compilation", + "anal creampie eating", + "anal cum", + "anal cumshot", + "anal destruction", + "anal dildo", + "anal extreme", + "anal fingering", + "anal fisting", + "anal fuck", + "anal fucking", + "anal gangbang", + "anal gape", + "anal granny", + "anal hardcore", + "anal hd", + "anal hentai", + "anal interracial", + "anal lesbians", + "anal licking", + "anal massage", + "anal mature", + "anal milf", + "anal orgasm", + "anal orgy", + "anal pain", + "anal party", + "anal porn", + "anal porno", + "anal pov", + "anal punishment", + "anal queen", + "anal sex", + "anal squirt", + "anal stretching", + "anal surprise", + "anal teen", + "anal teens", + "anal threesome", + "anal torture", + "anal training", + "anal video", + "anal virgin", + "anastasia christ", + "andi anderson", + "android porn", + "anetta keys", + "anette dawn", + "angel dark", + "angel eyes", + "angel long", + "angel porn", + "angelica sin", + "angelina crow", + "angelina valentine", + "animal fuck", + "animated porn", + "animated sex", + "anime hentai", + "anime lesbians", + "anime porn", + "anime porno", + "anime sex", + "anita blonde", + "anita dark", + "ann marie", + "anna malle", + "anna nova", + "annette schwarz", + "annie cruz", + "april flowers", + "arab cock", + "arab porn", + "arab sex", + "arabic sex", + "aria giovanni", + "ariana jollee", + "asa akira", + "ashley blue", + "ashli orion", + "ashlynn brooke", + "asia carrera", + "asian amateur", + "asian anal", + "asian bbw", + "asian handjob", + "asian lesbian", + "asian lesbian sex", + "asian lesbians", + "asian massage", + "asian massage porn", + "asian massage sex", + "asian massage videos", + "asian milf", + "asian porn", + "asian sex", + "asian sex massage", + "asian sex video", + "asian sex videos", + "asian street meat", + "asian teen", + "ass fuck", + "ass licking", + "ass porn", + "ass to mouth", + "ass xxx", + "aubrey addams", + "audrey bitoni", + "audrey hollander", + "aurora jolie", + "aurora snow", + "austin kincaid", + "ava devine", + "ava lauren", + "ava rose", + "avena lee", + "avy scott", + "ayana angel", + "babysitter porn", + "babysitter sex", + "backroom casting", + "backroom casting couch", + "backroom facials", + "bang bus", + "bang my wife", + "bangbros.com", + "bbw anal", + "bbw creampie", + "bbw pissing", + "bbw porn", + "bbw sex", + "bbw teen", + "bdsm porn", + "beach sex", + "beautiful ass", + "beautiful porn", + "beautiful pussy", + "beautiful sex", + "beautiful tits", + "beauty dior", + "behind the scenes", + "best ass", + "best blow job", + "best blow job ever", + "best blowjob", + "best blowjob ever", + "best free porn", + "best free porn sites", + "best handjob", + "best milf", + "best porn", + "best porn ever", + "best porn tube", + "best porno", + "best tits ever", + "bi porn", + "bianca pureheart", + "big ass", + "big ass anal", + "big ass porn", + "big asses", + "big black ass", + "big black booty", + "big black cock", + "big black dick", + "big black tits", + "big boobies", + "big boobs", + "big boobs videos", + "big booty", + "big booty porn", + "big butts", + "big clit", + "big cock", + "big cock porn", + "big cocks", + "big dick", + "big dick porn", + "big dicks", + "big natural tits", + "big nipples", + "big porn", + "big pussy", + "big pussy lips", + "big tit", + "big tit porn", + "big tits", + "big tits at school", + "big tits milf", + "big tits porn", + "big tits videos", + "big titties", + "biggest tits", + "bikini porn", + "bisexual porn", + "bisexual threesome", + "bizarre porn", + "black anal", + "black ass", + "black bbw", + "black booty", + "black cock", + "black cunt", + "black dick", + "black gang bang", + "black gangbang", + "black gf", + "black girl", + "black girl porn", + "black lesbian porn", + "black lesbians", + "black milf", + "black milfs", + "black porn", + "black porno", + "black pussy", + "black sex", + "black squirt", + "black teen", + "black tits", + "blacks on blondes", + "bleach porn", + "blonde porn", + "blonde pussy", + "blonde sex", + "blonde teen", + "blow job", + "blow jobs", + "blowjob compilation", + "blowjob videos", + "blue angel", + "bobbi starr", + "bollywood porn", + "bondage porn", + "bondage sex", + "boobs porn", + "brandy talore", + "brandy taylor", + "brazil porn", + "brazilian ass", + "brazilian booty", + "brazilian porn", + "brea bennett", + "bree olsen", + "briana banks", + "brianna beach", + "brianna frost", + "brianna love", + "british bbw", + "british porn", + "britney spears", + "britney stevens", + "brittany andrews", + "brittney skye", + "broke straight boys", + "brooke banner", + "brooke haven", + "brooke hun", + "brooke hunter", + "brooke skye", + "brother and sister", + "bruna ferraz", + "brutal porn", + "brutal sex", + "brynn tyler", + "bubble butt", + "busty lesbians", + "busty milf", + "busty russians", + "busty teen", + "butt fuck", + "cam porn", + "camera inside vagina", + "can he score", + "candace von", + "candy manson", + "car sex", + "carly parker", + "carmel moore", + "carmella bing", + "carmen hayes", + "carmen kinsley", + "carmen luvana", + "carton porn", + "cartoon hentai", + "cartoon network porno", + "cartoon porn", + "cartoon porn videos", + "cartoon porno", + "cartoon sex", + "cartoon sex video", + "cartoon sex videos", + "cartoon xxx", + "cash for sex", + "cassandra cruz", + "cassie young", + "casting anal", + "casting couch", + "casting porno", + "casting sex", + "casting xxx", + "catwoman porn", + "caught masturbating", + "celeb porn", + "celeb sex tapes", + "celebrity nudes", + "celebrity porn", + "celebrity porno", + "celebrity sex scenes", + "celebrity sex tape", + "celebrity sex tapes", + "celebrity sex videos", + "charley chase", + "charlie laine", + "charlotte stokely", + "charmane star", + "chasey lain", + "chat porno", + "chatroulette sex", + "chav porn", + "chayse evans", + "cheating wife", + "cheerleader porn", + "chicks with dicks", + "china porn", + "chinese porn", + "chinese pussy", + "chloe dior", + "chloe lamb", + "christina bella", + "christmas porn", + "christy canyon", + "chubby milf", + "chubby porn", + "chubby teen", + "chyanne jacobs", + "cindy crawford", + "cindy hope", + "claire dames", + "classic porn", + "classy porn", + "claudia rossi", + "close up pussy", + "cock cum", + "cock docking", + "cock hero", + "cock massage", + "cock sounding", + "cock stuffing", + "cock sucking", + "cody lane", + "college dorm", + "college fuck", + "college fuck fest", + "college girls", + "college orgy", + "college party", + "college porn", + "college rules", + "college sex", + "college sex party", + "college sex videos", + "college sluts", + "colombian porn", + "comic porn", + "comics porno", + "comics xxx", + "compilation porn", + "cory everson", + "cosplay hentai", + "cosplay porn", + "cosplay porno", + "cosplay sex", + "cougar porn", + "cougar sex", + "couple sex", + "courtney cummz", + "courtney simpson", + "creampie angels", + "creampie cleanup", + "creampie compilation", + "creampie eating", + "creampie gangbang", + "creampie porn", + "creampie surprise", + "creampie thais", + "creampie videos", + "creamy orgasm", + "creamy pussy", + "crissy moran", + "crystal clear", + "cuckold amateur", + "cuckold cleanup", + "cuckold creampie", + "cuckold interracial", + "cuckold porn", + "cuckold wife", + "cum compilation", + "cum control", + "cum down throat", + "cum fiesta", + "cum fountain", + "cum in ass", + "cum in condom", + "cum in mouth", + "cum in mouth compilation", + "cum in my mouth", + "cum in pussy", + "cum in throat", + "cum inside", + "cum on my tits", + "cum on pussy", + "cum on tits", + "cum porn", + "cum shot", + "cum shots", + "cum swallow", + "cum swap", + "cum twice", + "cum+inside", + "cumshot compilation", + "cumshot surprise", + "cute porn", + "cute teen", + "czech porn", + "daisy marie", + "dana dearmond", + "dance porn", + "dancing bear", + "dancing bear fuck", + "dancing porn", + "dani woodward", + "daniella rush", + "danish porn", + "daphne rosen", + "dare dorm", + "daria glover", + "daughter destruction", + "dd tits", + "deep anal", + "deep anal dildo", + "deep anal fisting", + "deep creampie", + "deep fisting", + "deep fuck", + "deep pussy", + "deep throat", + "deepthroat blowjob", + "deepthroat compilation", + "deepthroat cum", + "deepthroat cumshot", + "deepthroat porn", + "deepthroat swallow", + "deepthroat videos", + "defloration videos", + "delilah strong", + "delotta brown", + "demon hentai", + "demon porn", + "desi sex videos", + "devil porn", + "devon lee", + "devon michaels", + "dexter porn", + "diamond foxxx", + "diaper porn", + "diaper sex", + "dick girls", + "dick in ass", + "dick in pussy", + "dick massage", + "dick sucking", + "dick sucking lips", + "dildo anal", + "dildo bike", + "dildo porn", + "dildo sex", + "dillan lauren", + "dirty anal", + "dirty ass to mouth", + "dirty latina maids", + "dirty masseur", + "dirty porn", + "dirty pussy", + "dirty sluts", + "dirty talk", + "dirty talk porn", + "doctor porn", + "dogging videos", + "doggy porn", + "doggy sex", + "doggystyle compilation", + "doggystyle porn", + "doggystyle sex", + "domination porn", + "dominican porn", + "donkey porn", + "dont cum inside me", + "dora venter", + "dorm invasion", + "dorm porn", + "double anal", + "double anal fisting", + "double blowjob", + "double creampie", + "double dick", + "double dildo", + "double fisting", + "double fuck", + "double handjob", + "double penetration", + "double penetration videos", + "double porn", + "double vaginal", + "down syndrome porn", + "dp anal", + "dp creampie", + "dp porn", + "dp sex", + "drawn hentai", + "dream porn", + "dream porno", + "drinking cum", + "dripping wet pussy", + "drunk anal", + "drunk college girls", + "drunk fuck", + "drunk girl fucked", + "drunk girl porn", + "drunk girl sex", + "drunk porn", + "drunk sex", + "drunk sluts", + "drunk wife", + "dry humping porn", + "dutch porn", + "dwarf porn", + "dwarf sex", + "dyanna lauren", + "dylan ryder", + "eat ass", + "eat cum", + "eat pussy", + "eating ass", + "eating creampie", + "eating cum", + "eating pussy", + "ebony amateur", + "ebony anal", + "ebony ass", + "ebony babes", + "ebony bbw", + "ebony big ass", + "ebony big tits", + "ebony blowjob", + "ebony boobs", + "ebony booty", + "ebony bukkake", + "ebony creampie", + "ebony deepthroat", + "ebony fuck", + "ebony gangbang", + "ebony girls", + "ebony lesbian porn", + "ebony lesbians", + "ebony milf", + "ebony milfs", + "ebony orgy", + "ebony porn", + "ebony porno", + "ebony pussy", + "ebony sex", + "ebony sex videos", + "ebony sexy", + "ebony sluts", + "ebony squirt", + "ebony squirting", + "ebony teen", + "ebony threesome", + "ebony tits", + "ebony videos", + "egypt sex", + "egyptian porn", + "eight tube", + "elbow deep fisting", + "elbow fisting", + "electro cum", + "electro orgasm", + "electro sex", + "elegant porn", + "elena grimaldi", + "elf hentai", + "elf porn", + "ellen saint", + "emma (mae)", + "emma heart", + "emo anal", + "emo blowjob", + "emo girl porn", + "emo lesbians", + "emo porn", + "emo porno", + "emo sex", + "emo sluts", + "enema porn", + "england porn", + "english dubbed hentai", + "english hentai", + "english porn", + "enormous cock", + "erotic blowjob", + "erotic cartoons", + "erotic lesbians", + "erotic massage", + "erotic massage video", + "erotic massage videos", + "erotic porn", + "erotic video", + "erotic videos", + "erotica for women", + "escort porn", + "ethiopia porn", + "ethiopian porn", + "euro porn", + "euro sex", + "eva angelina", + "eve angel", + "eve lawrence", + "evelyn lin", + "ex gf videos", + "ex girlfriend", + "ex girlfriend porn", + "ex girlfriend revenge", + "ex girlfriend sex", + "exotic porn", + "explicit sex scenes", + "exploited babysitters", + "exploited college", + "exploited college girls", + "extra big dicks", + "extreme anal", + "extreme ass", + "extreme asses", + "extreme bdsm", + "extreme bikini", + "extreme blowjob", + "extreme bondage", + "extreme boobs", + "extreme bukkake", + "extreme cfnm", + "extreme creampie", + "extreme deepthroat", + "extreme dildo", + "extreme fisting", + "extreme fucking", + "extreme gagging", + "extreme gangbang", + "extreme hardcore porn", + "extreme orgasm", + "extreme porn", + "extreme pussy", + "extreme sex", + "extreme squirting", + "face fuck", + "facial compilation", + "facial cumshot", + "fairy tail porn", + "fake agent", + "fake tits", + "famous porn", + "famous toons", + "famous toons facial", + "fart porn", + "fat ass porn", + "fat black ass", + "fat cock", + "fat girl porn", + "fat lesbians", + "fat porn", + "fat pussy", + "fat tits", + "faye reagan", + "faye valentine", + "female agent", + "female ejaculation", + "female friendly porn", + "female masturbation", + "female orgasm", + "female porn", + "femdom porn", + "fetish porn", + "ffm porn", + "fiesta porno", + "filipina porn", + "filipino porn", + "film porn", + "film porno", + "films porno", + "final fantasy porn", + "fingering porn", + "finnish porn", + "first anal", + "first anal sex", + "first fuck", + "first time", + "first time anal", + "first time fucking", + "first time lesbian", + "first time porn", + "first time sex", + "first time sex movies", + "first time sex video", + "first time sex videos", + "fist fucking", + "fisting anal", + "fitness porn", + "fleshlight video", + "flexible porn", + "flick shagwell", + "flower tucci", + "foot fetish", + "foot fetish porn", + "foursome porn", + "foursome sex", + "francesca le", + "freaky porn", + "free adult movies", + "free asian porn", + "free black lesbian porn", + "free black porn", + "free cartoon porn", + "free cartoon porn videos", + "free celebrity porn", + "free celebrity sex tapes", + "free download sex videos", + "free ebony porn", + "free hot porn", + "free hot sex", + "free internet porn", + "free latina porn", + "free lesbian porn", + "free massage porn", + "free mobile porn", + "free movies", + "free movies porn", + "free online porn", + "free online porn movies", + "free online sex", + "free online sex videos", + "free porn clips", + "free porn films", + "free porn games", + "free porn massage", + "free porn movies", + "free porn sex videos", + "free porn sites", + "free porn stream", + "free porn tube", + "free porn videos", + "free porn vids", + "free porn websites", + "free porno movies", + "free porno tube", + "free porno videos", + "free pornography", + "free pron", + "free sex clips", + "free sex films", + "free sex sites", + "free sex tubes", + "free sex video", + "free sex videos", + "free sex vids", + "free streaming porn", + "frei sex", + "french amateur", + "french anal", + "french milf", + "french porn", + "french porno", + "french sex", + "friends hot mom", + "friends mom", + "frottage videos", + "ftm porn", + "fuck ass", + "fuck for money", + "fuck girl", + "fuck hard", + "fuck machine", + "fuck me", + "fuck me hard", + "fuck my ass", + "fuck my girlfriend", + "fuck my pussy", + "fuck my wife", + "fuck porn", + "fuck pussy", + "fuck videos", + "fucked from behind", + "fucked hard", + "fucked hard 18", + "fucked in public", + "fucked up porn", + "fucking ass", + "fucking girls", + "fucking hard", + "fucking in public", + "fucking machine", + "fucking machines", + "fucking mom", + "fucking my wife", + "fucking porn", + "fucking pussy", + "fucking teen", + "fucking the babysitter", + "fucking videos", + "full free porn", + "full movie", + "full porno", + "fun porn", + "funny porn", + "furry hentai", + "furry porn", + "fursuit porn", + "fursuit sex", + "futanari hentai", + "futanari porn", + "gag porn", + "gagging porn", + "game porn", + "gang bang", + "gangbang creampie", + "gangbang my wife", + "gangbang porn", + "gangbang wife", + "gaping ass", + "gaping pussy", + "geek porn", + "gen padova", + "georgia peach", + "german amateur", + "german anal", + "german mature", + "german milf", + "german porn", + "german porno", + "german sex", + "german teen", + "gf porn", + "gf revenge", + "ghana porn", + "ghetto booty", + "ghetto porn", + "gia paloma", + "gianna lynn", + "gianna michaels", + "giant cock", + "giant dildo", + "gigantic tits", + "gilf porn", + "gina lynn", + "gina ryder", + "gina wild", + "ginger lee", + "ginger lynn", + "ginger porn", + "girl caught masturbating", + "girl cum", + "girl cumming", + "girl fuck", + "girl fucking", + "girl having sex", + "girl masterbating", + "girl masturbating", + "girl masturbation", + "girl next door porn", + "girl on girl", + "girl on girl action", + "girl on girl porn", + "girl on girl sex", + "girl orgasm", + "girl pissing", + "girl porn", + "girl pussy", + "girl sex", + "girl squirting", + "girlfriend blowjob", + "girlfriend porn", + "girlfriend revenge", + "girlfriend sex", + "girlfriend videos", + "girls cumming", + "girls do porn", + "girls fingering", + "girls fucking", + "girls fucking girls", + "girls gone wild", + "girls hunting girls", + "girls in stockings", + "girls peeing", + "girls scissoring", + "girls squirting", + "girls with big tits", + "glamour porn", + "glory hole", + "gloryhole creampie", + "gloryhole porn", + "gloryhole swallow", + "gold porn", + "gonzo porn", + "good fuck", + "good porn", + "good pussy", + "goth porn", + "gothic porn", + "grandma porn", + "grandma sex", + "grandpa porn", + "granny anal", + "granny ass", + "granny fisting", + "granny fuck", + "granny gangbang", + "granny lesbian", + "granny orgy", + "granny porn", + "granny sex", + "granny video", + "great ass", + "great porn", + "great tits", + "greek porn", + "greek porno", + "greek sex", + "group fuck", + "group porn", + "group sex", + "group sex porn", + "group sex videos", + "guy porn", + "gym porn", + "gyno exam", + "gyno porn", + "gypsy porn", + "hairy creampie", + "hairy cunts", + "hairy porn", + "hairy pussy", + "hairy teen", + "haley paige", + "hand domination", + "hand job", + "hand jobs", + "handjob compilation", + "handjob porn", + "handjob videos", + "hands free cum", + "hands free ejaculation", + "hands free orgasm", + "handsfree cum", + "hanna hilton", + "hannah harper", + "happy tugs", + "hard cock", + "hard core porn", + "hard fuck", + "hardcore fucking", + "hardcore junky", + "hardcore party", + "hardcore porn", + "hardcore porno", + "hardcore sex", + "hardcore sex videos", + "harmony (bliss)", + "harmony rose", + "havana ginger", + "hd anal", + "hd passion", + "hd porn", + "hd porno", + "hd pussy", + "hd xxx", + "heather brooke", + "hegre-art", + "hentai 3d", + "hentai anal", + "hentai anime", + "hentai bondage", + "hentai dragon ball", + "hentai futanari", + "hentai hd", + "hentai lesbian", + "hentai manga", + "hentai porn", + "hentai porno", + "hentai sex", + "hentai sex videos", + "hentai tentacle", + "hentai video", + "hentai videos", + "her sweet hand", + "hermaphrodite sex", + "hidden cam", + "hidden cam porn", + "hidden camera", + "hidden camera sex", + "hidden sex", + "high quality porn", + "high school porn", + "hijab porn", + "hijab sex", + "hillary scott", + "hipster porn", + "hitomi tanaka", + "holly body", + "holly halston", + "holly wellin", + "hollywood porn", + "home made porn", + "home made sex videos", + "homemade anal", + "homemade porn", + "homemade sex videos", + "homemade threesome", + "hood porn", + "hooker porn", + "horny girl", + "horny milf", + "horny moms", + "horny wife", + "horny women", + "hot and mean", + "hot asian", + "hot ass", + "hot blonde", + "hot blondes", + "hot brunette", + "hot free porn", + "hot free sex", + "hot fuck", + "hot fucking", + "hot girl porn", + "hot girl sex", + "hot hentai", + "hot indian porn", + "hot latina", + "hot lesbian porn", + "hot lesbian sex", + "hot lesbians", + "hot massage", + "hot mature", + "hot milfs", + "hot mom", + "hot mom porn", + "hot moms", + "hot movies", + "hot naked women", + "hot porn", + "hot porn movies", + "hot porn videos", + "hot porno", + "hot sex", + "hot sex movies", + "hot sex porn", + "hot sex video", + "hot sex videos", + "hot sluts", + "hot tits", + "hottest porn", + "housewife porn", + "how to eat pussy", + "how to fuck", + "how to make a girl squirt", + "how to squirt", + "huge ass", + "huge boobs", + "huge clit", + "huge cock", + "huge cumshot", + "huge dick", + "huge dildo", + "huge natural tits", + "huge nipples", + "huge pussy", + "huge tits", + "hulk hogan", + "i fucked my teacher", + "i have a wife", + "i know that girl", + "icelandic porn", + "impregnation porn", + "in the ass", + "in the crack", + "inari vachs", + "incredible porn", + "india porn", + "india porno", + "india pussy", + "india sex video", + "india summer anal", + "india summer porn", + "india uncovered", + "india xxx", + "indian actress", + "indian anal", + "indian aunty", + "indian babes", + "indian big tits", + "indian boobs", + "indian cock", + "indian college sex", + "indian couple sex", + "indian creampie", + "indian defloration", + "indian fuck", + "indian fucking", + "indian gangbang", + "indian girl fucked", + "indian girl fucking", + "indian girl mms", + "indian girl porn", + "indian girl sex", + "indian group sex", + "indian hidden cam", + "indian home made sex videos", + "indian hot sex", + "indian lesbian sex", + "indian lesbians", + "indian maid porn", + "indian milf", + "indian porn", + "indian porn star", + "indian porn stars", + "indian porn video", + "indian porn videos", + "indian porno", + "indian pornstars", + "indian pussy", + "indian real sex", + "indian sex", + "indian sex scandals", + "indian sex video", + "indian sex videos", + "indian sluts", + "indian teen", + "indian tits", + "indian village sex", + "indian xxx", + "indie porn", + "inflatable butt plug", + "inflatable dildo", + "innocent porn", + "insane porn", + "insertion porn", + "inside pussy", + "instruction porn", + "instructional sex videos", + "intense orgasm", + "interactive porn", + "interesting porn", + "internal creampie", + "internal cumshot", + "internet porn", + "interracial anal", + "interracial compilation", + "interracial creampie", + "interracial cuckold", + "interracial dp", + "interracial gangbang", + "interracial lesbians", + "interracial milf", + "interracial orgy", + "interracial porn", + "interracial porn videos", + "interracial sex videos", + "interracial swingers", + "interracial threesome", + "interracial videos", + "interracial wife", + "interracial xxx", + "interview porn", + "ipad porn", + "iran porn", + "iranian sex", + "iraq porn", + "irish porn", + "isabel ice", + "isabella soprano", + "isis love", + "isis taylor", + "israel porn", + "israel sex", + "israeli porn", + "israeli sex", + "italian amateur", + "italian anal", + "italian mature", + "italian milf", + "italian porn", + "italian porn movies", + "italian porno", + "italian pornstars", + "italian pussy", + "italian sex", + "italian swingers", + "jack napier", + "jack off", + "jacking off", + "jaclyn case", + "jada fire", + "jamaican porn", + "jamaican pussy", + "jamie brooks", + "jamie elle", + "jana cova", + "jane darling", + "janine lindemulder", + "jap porn", + "japan lesbian", + "japan porn", + "japan porno", + "japan sex video", + "japanese amateur", + "japanese anal", + "japanese av", + "japanese bdsm", + "japanese beauties", + "japanese big tits", + "japanese black", + "japanese blowjob", + "japanese bukkake", + "japanese creampie", + "japanese daddy", + "japanese family", + "japanese game", + "japanese game show", + "japanese gangbang", + "japanese girl", + "japanese girl tube", + "japanese granny", + "japanese handjob", + "japanese interracial", + "japanese lesbian", + "japanese lesbian massage", + "japanese lesbians", + "japanese massage", + "japanese masturbation", + "japanese mature", + "japanese milf", + "japanese mom", + "japanese mom and son", + "japanese mother", + "japanese nurse", + "japanese orgy", + "japanese porn", + "japanese school", + "japanese school girl", + "japanese schoolgirl porn", + "japanese sex", + "japanese sex show", + "japanese sex video", + "japanese sex videos", + "japanese squirt", + "japanese student", + "japanese teacher", + "japanese teen", + "japanese teen fuck", + "japanese teen porn", + "japanese uncensored", + "japanese wife", + "japanese xxx", + "jasmin st. claire", + "jasmine black", + "jasmine byrne", + "jasmine rouge", + "jasmine tame", + "jayden jaymes", + "jayna oso", + "jazmine cashmere", + "jeanna fine", + "jelena jensen", + "jenaveve jolie", + "jenna haze", + "jenna jameson", + "jenna presley", + "jenni lee", + "jennifer luv", + "jennifer stone", + "jenny hendrix", + "jerk me off", + "jerk off", + "jerk off encouragement", + "jerk off instruction", + "jerk off instructions", + "jerk off porn", + "jerking off", + "jerking off in public", + "jerkoff instruction", + "jerky girls", + "jessi summers", + "jessica bangkok", + "jessica drake", + "jessica jaymes", + "jessica lynn", + "jessica moore", + "jessica rabbit porn", + "jewish porn", + "jewish sex", + "jill kelly", + "john holmes", + "johnni black", + "jr carrington", + "juggalette porn", + "juicy ass", + "juicy boobs", + "juicy pussy", + "kacey jordan", + "kagney linn karter", + "kama sutra", + "kamasutra porn", + "kapri styles", + "kardashian sex tape", + "karen lancaume", + "karina kay", + "kathleen kruz", + "katie morgan", + "katja kassin", + "katrina kraven", + "kayden kross", + "kayla marie", + "kaylani lei", + "kelly b", + "kelly divine", + "kelly kline", + "kelly star", + "kelly trump", + "kelly wells", + "kenyan porn", + "keri sable", + "keri windsor", + "kianna dior", + "kiki daire", + "kim kardashian", + "kim kardashian sex tape", + "kind porn", + "kink.com", + "kinky lesbians", + "kinky milf", + "kinky porn", + "kinky sex", + "kinky xxx", + "kinzie kenner", + "kira kener", + "kissing lesbians", + "kissing porn", + "kitchen porn", + "kitchen sex", + "kobe tai", + "korea sex", + "korean amateur", + "korean anal", + "korean porn", + "korean pussy", + "korean sex", + "korean student", + "korean teen", + "korean teen porn", + "korean webcam", + "korean wife", + "kristal summers", + "kristina rose", + "krystal steal", + "kylie ireland", + "lacey duvalle", + "lacie heart", + "lactating porn", + "lactating tits", + "lady porn", + "lana croft", + "lani lei", + "lanny barbie", + "lap dance", + "lap dance porn", + "large breasts", + "large clit", + "large cocks", + "large porn", + "large tits", + "latest porn", + "latin ass", + "latin porn", + "latin xxx", + "latina anal", + "latina ass", + "latina booty", + "latina lesbians", + "latina milf", + "latina porn", + "latina pussy", + "latina teen", + "latinas porn", + "latinas porno", + "latinas xxx", + "latino porn", + "latino sex", + "laura angel", + "laura lion", + "lauren phoenix", + "leah jaye", + "leah luv", + "leanna heart", + "lee stone", + "legal porn", + "leggings porn", + "lela star", + "lelu love", + "lesbian 69", + "lesbian action", + "lesbian amateur", + "lesbian anal", + "lesbian anal licking", + "lesbian ass", + "lesbian ass licking", + "lesbian babes", + "lesbian babysitter", + "lesbian bdsm", + "lesbian big tits", + "lesbian bondage", + "lesbian breastfeeding", + "lesbian bukkake", + "lesbian cheerleaders", + "lesbian domination", + "lesbian facesitting", + "lesbian feet", + "lesbian first time", + "lesbian fisting", + "lesbian foot fetish", + "lesbian fuck", + "lesbian gangbang", + "lesbian girls", + "lesbian grannies", + "lesbian grinding", + "lesbian hd", + "lesbian hentai", + "lesbian hot", + "lesbian kissing", + "lesbian licking", + "lesbian lovers", + "lesbian massage", + "lesbian masturbation", + "lesbian milf", + "lesbian orgasm", + "lesbian orgy", + "lesbian party", + "lesbian piss", + "lesbian pissing", + "lesbian porn", + "lesbian porno", + "lesbian pussy", + "lesbian pussy licking", + "lesbian scissoring", + "lesbian seduce", + "lesbian seduction", + "lesbian sex", + "lesbian sex games", + "lesbian sex videos", + "lesbian sisters", + "lesbian slave", + "lesbian spanking", + "lesbian squirt", + "lesbian squirting", + "lesbian strap on", + "lesbian strapon", + "lesbian teacher", + "lesbian threesome", + "lesbian tribbing", + "lesbian twins", + "lesbian video", + "lesbian videos", + "lesbian wrestling", + "lesbian xxx", + "lesbians fucking", + "lesbians grinding", + "lesbians having sex", + "lesbians humping", + "lesbians making love", + "lesbians making out", + "lesbians scissoring", + "lesbians sex", + "lesbians squirting", + "lesbians tribbing", + "lesbians videos", + "lex steele", + "lexi belle", + "lexi love", + "lexington steele", + "lez porn", + "lezley zen", + "lichelle marie", + "lick my pussy", + "lick pussy", + "licking pussy", + "lily thai", + "lindsey meadows", + "lingerie porn", + "lisa ann", + "lisa lipps", + "lisa sparxxx", + "little pussy", + "little tits", + "london keys", + "long porn", + "longest cumshot", + "loona lux", + "loose pussy", + "lorena sanchez", + "love porn", + "lucie theodorova", + "lucy lee", + "lucy thai", + "luna lane", + "luscious lopez", + "machine porn", + "machine sex", + "macho fucker", + "mackenzee pierce", + "madison ivy", + "maid porn", + "malay porn", + "male porn", + "man eating pussy", + "mandingo anal", + "mandingo porn", + "mandingo xxx", + "manga hentai", + "manga porno", + "manga sex", + "maria bellucci", + "maria ozawa", + "mariah milano", + "marie luv", + "mark ashley", + "marquetta jewel", + "marry queen", + "mary anne", + "mason moore", + "mason storm", + "massage creep", + "massage fuck", + "massage lesbian", + "massage orgasm", + "massage parlor", + "massage penis", + "massage porn", + "massage sex", + "massage sex video", + "massive boobs", + "massive cock", + "massive creampie", + "massive cumshot", + "massive tits", + "mature amateur", + "mature anal", + "mature blowjob", + "mature boobs", + "mature dp", + "mature ladies", + "mature lesbian", + "mature masturbation", + "mature milf", + "mature orgasm", + "mature orgy", + "mature porn", + "mature sex", + "mature swingers", + "mature women", + "mature young", + "maya hills", + "mckenzie miles", + "medical voyeur", + "medieval porn", + "melissa lauren", + "melrose foxxx", + "memphis monroe", + "men at play", + "men eating pussy", + "men fucking women", + "men licking pussy", + "men masterbating", + "messy anal", + "mexican cock", + "mexican girls", + "mexican porn", + "mexican pussy", + "mexican sex", + "mexican xxx", + "mia bangg", + "mia rose", + "micah moore", + "michelle may", + "michelle maylene", + "michelle wild", + "midget porn", + "midget sex", + "mika tan", + "miko lee", + "milf amateur", + "milf anal", + "milf ass", + "milf big tits", + "milf blowjob", + "milf boobs", + "milf creampie", + "milf gangbang", + "milf handjob", + "milf hd", + "milf hunter", + "milf lesbian", + "milf lesbian porn", + "milf lesbians", + "milf massage", + "milf mature", + "milf next door", + "milf orgasm", + "milf orgy", + "milf porn", + "milf pussy", + "milf sex", + "milf squirt", + "milf teacher", + "milf threesome", + "milf tits", + "milf young", + "military porn", + "milking tits", + "milky tits", + "millian blu", + "mindy main", + "mindy vega", + "missionary porn", + "missy monroe", + "missy stone", + "misti love", + "mistress porn", + "misty stone", + "mmf porn", + "mobile porn", + "model porn", + "mom anal", + "mom and daughter", + "mom and son", + "mom fuck", + "mom fucks", + "mom fucks son", + "mom hot", + "mom porn", + "mom pov", + "mom son", + "mommy loves pussy", + "mommy porn", + "moms bang teens", + "mone divine", + "money talks", + "monica mattos", + "monica mayhem", + "monica santhiago", + "monica sweet", + "monica sweetheart", + "monique alexander", + "monster cock", + "monster cock anal", + "monster curves", + "monster dick", + "monster dildo", + "monster porn", + "monster sex", + "mother and daughter", + "mother and son", + "mother daughter", + "mother in law", + "mother in law porn", + "mother porn", + "mother son", + "mouth cum", + "mouth fuck", + "movie porn", + "movie porno", + "movie sex", + "movie xxx", + "movies porn", + "movies porno", + "movies sex", + "movies xxx", + "mr marcus", + "multiple creampie", + "mum porn", + "muscle girl", + "muscle porn", + "mushroom cock", + "muslim porn", + "mutual masterbation", + "mutual masturbation", + "my first sex teacher", + "my friends hot mom", + "my sisters hot friend", + "my wifes mom", + "mya diamond", + "mya g", + "mya nichole", + "nadia hilton", + "nadia styles", + "naked asian girls", + "naked ass", + "naked beach", + "naked blonde", + "naked boobs", + "naked celebrities", + "naked chicks", + "naked college girls", + "naked emo girls", + "naked girl", + "naked housewives", + "naked in public", + "naked latinas", + "naked lesbians", + "naked male celebs", + "naked male stars", + "naked massage", + "naked milf", + "naked news videos", + "naked porn", + "naked pussy", + "naked sluts", + "naked tits", + "naked twerking", + "naked twister", + "naked whores", + "naked women", + "naked workout", + "naked wrestling", + "naked yoga", + "naomi russell", + "naruto hentai", + "naruto porn", + "naruto xxx", + "nasty lesbians", + "nasty porn", + "nasty sex", + "nasty sluts", + "nasty xxx", + "natalia rossi", + "natasha nice", + "native american porn", + "native porn", + "natural big tits", + "natural boobs", + "natural porn", + "natural tits", + "naughty allie", + "naughty america", + "naughty girls", + "naughty milf", + "naughty office", + "naughty porn", + "nautica thorn", + "nazi porn", + "neighbour porn", + "nerd porn", + "nessa devil", + "net video girls", + "new sex video", + "new sex videos", + "nice ass", + "nice asses", + "nice big ass", + "nice big tits", + "nice blowjob", + "nice boobs", + "nice breasts", + "nice cunt", + "nice fuck", + "nice handjob", + "nice natural tits", + "nice porn", + "nice pussy", + "nice sex", + "nice tits", + "nicole graves", + "nicole ray", + "nicole sheridan", + "nigerian porn", + "night porn", + "nikita denise", + "nikki benz", + "nikki hunter", + "nikki n", + "nikki rhodes", + "nina hartley", + "nina mercedez", + "ninja porn", + "nipple porn", + "nipple sucking", + "nipple torture", + "norway porn", + "norwegian porn", + "nubile porn", + "nude aerobics", + "nude asian", + "nude beach", + "nude beach sex", + "nude blonde", + "nude celebrities", + "nude celebs", + "nude in public", + "nude male celebs", + "nude massage", + "nude porn", + "nude redheads", + "nude tube", + "nude women", + "nude wrestling", + "nude xxx", + "nude yoga", + "nudist sex", + "nun porn", + "nun sex", + "nurse fuck", + "nurse handjob", + "nurse porn", + "nurse sex", + "nuru massage", + "nylon footjob", + "nylon porn", + "nympho porn", + "odd porn", + "office blowjob", + "office fuck", + "office lesbians", + "office milf", + "office porn", + "office sex", + "office sluts", + "oil ass", + "oil fuck", + "oil massage porn", + "oil massage sex", + "oil orgy", + "oil overload", + "oil porn", + "oil sex", + "oiled anal", + "oiled ass", + "oiled boobs", + "oiled porn", + "oiled pussy", + "oiled sex", + "oiled tits", + "oiled up", + "oily massage", + "oily porn", + "oily sex", + "old anal", + "old and young", + "old couple sex", + "old farts young tarts", + "old fuck", + "old fuck young", + "old granny fucking", + "old granny porn", + "old hairy pussy", + "old ladies fucking", + "old ladies porn", + "old lady porn", + "old lesbian porn", + "old lesbian sex", + "old lesbians", + "old man", + "old man creampie", + "old man fuck young girl", + "old man fucking", + "old man porn", + "old man sex", + "old man young girl", + "old mature porn", + "old men porn", + "old milf", + "old people porn", + "old porn", + "old porno", + "old pussy", + "old pussy exam", + "old school porn", + "old sex", + "old sluts", + "old woman porn", + "old woman sex", + "old women porn", + "old women sex", + "old young", + "old young porn", + "older lesbians", + "older milf", + "older porn", + "older women porn", + "olivia del rio", + "omegle boobs", + "omegle girls", + "omegle porn", + "omegle sex", + "omegle tits", + "omegle videos", + "one piece hentai", + "onion booty", + "online porn", + "online porno", + "oops porn", + "open ass", + "open pussy", + "oral compilation", + "oral creampie", + "oral creampie compilation", + "oral cumshot", + "oral porn", + "oral porno", + "oral queens", + "oral sex", + "oral xxx", + "orgasm compilation", + "orgasm contractions", + "orgasm denial", + "orgasm machine", + "orgasm massage", + "orgasm movies", + "orgasm porn", + "orgasm torture", + "orgy movies", + "orgy party", + "orgy porn", + "orgy sex", + "orgy video", + "oriental porn", + "oriental sex", + "outdoor blowjob", + "outdoor bondage", + "outdoor fuck", + "outdoor handjob", + "outdoor nudity", + "outdoor porn", + "outdoor sex", + "outdoor xxx", + "over 60 porn", + "painful anal", + "pakistani porn", + "pamela anderson sex tape", + "panty pee", + "panty pissing", + "pantyhose porn", + "paris hilton", + "paris hilton sex tape", + "paris porn", + "parody porn", + "party porn", + "party sex", + "passed out", + "passion hd", + "passionate porn", + "passionate sex", + "patricia petite", + "paulina james", + "pee panties", + "peeing panties", + "penis massage", + "penny flame", + "perfect ass", + "perfect boobs", + "perfect girls", + "perfect pussy", + "perfect tits", + "perky tits", + "persia decarlo", + "persian porn", + "peter north", + "petite teen", + "phat ass", + "phoenix marie", + "pick up", + "pick up porn", + "pierre woodman", + "pink pussy", + "pinky porn", + "pirates porn", + "piss porn", + "pissing compilation", + "please bang my wife", + "please fuck my wife", + "pokemon hentai", + "pokemon porn", + "pokemon xxx", + "polish porn", + "porn bloopers", + "porn for women", + "porn massage", + "porn movies", + "porn online", + "porn sex videos", + "porn stream", + "porn streaming", + "porn tube", + "porn tube sex", + "porn tubes", + "porno stars", + "porno streaming", + "pornstar punishment", + "pov blowjob", + "pov porn", + "pregnant porn", + "priya rai", + "prostate massage", + "prostate milking", + "prostate orgasm", + "public agent", + "public disgrace", + "public fuck", + "public invasion", + "public masturbation", + "public pickups", + "public porn", + "public sex", + "public wank", + "puerto rican porn", + "puffy nipples", + "puffy pussy", + "puma swede", + "punish porn", + "punishment porn", + "pure mature", + "pussy cum", + "pussy eating", + "pussy fuck", + "pussy licking", + "pussy massage", + "pussy porn", + "pussy pump", + "pussy sex", + "quad penetration", + "quadriplegic porn", + "quadriplegic sex", + "quadruple penetration", + "quadruple vaginal penetration", + "quality porn", + "quarterback porn", + "queef compilation", + "queef porn", + "queefing porn", + "queen anal", + "queen blowjob", + "queen diva porn", + "queen fucking", + "queen handjob", + "queen porn", + "queen sheba porn", + "queening video", + "queens porn", + "quick anal", + "quick bj", + "quick blowjob", + "quick creampie", + "quick cum", + "quick cum compilation", + "quick cummer", + "quick fuck", + "quick fucking", + "quick handjob", + "quick head", + "quick office fuck", + "quick office sex", + "quick orgasm", + "quick porn", + "quick sex", + "quick wank porn", + "quickest cum ever", + "quickest cumshot", + "quickest fuck ever", + "quickest handjob", + "quickie anal", + "quickie blowjob", + "quickie creampie", + "quickie fuck", + "quickie handjob", + "quickie porn", + "quickie sex", + "quicksand porn", + "quicky porn", + "quiet fuck", + "quiet porn", + "quiet sex", + "quirky porn", + "quivering orgasm", + "quivering pussy", + "rachel roxxx", + "rachel starr", + "rare porn", + "rave porn", + "raven riley", + "raw fuck club", + "raw porn", + "real amateur porn", + "real couple sex", + "real female orgasm", + "real first anal", + "real gf porn", + "real girls fucked in public", + "real homemade porn", + "real lesbian porn", + "real lesbians", + "real life porn", + "real milf", + "real orgasm", + "real porn", + "real sex", + "real slut party", + "real slut party videos", + "real squirt", + "real swingers", + "real tits", + "realistic porn", + "reality kings", + "reality porn", + "reality sex", + "rebeca linares", + "rebecca linares", + "red hair porn", + "red hair pussy", + "red hairy pussy", + "red head", + "red hot lauren", + "red tube", + "redhead anal", + "redhead big tits", + "redhead blowjob", + "redhead creampie", + "redhead fuck", + "redhead lesbians", + "redhead milf", + "redhead porn", + "redhead pov", + "redhead pussy", + "redhead sex", + "redhead teen", + "redhead tits", + "redneck porn", + "regina ice", + "renae cruz", + "retro porn", + "retro porno", + "retro sex", + "Return to top 20", + "revenge porn", + "revenge sex", + "reverse cowgirl", + "reverse cowgirl porn", + "reverse cowgirl pov", + "reverse gangbang", + "ricki white", + "rico strong", + "ridiculous orgasm", + "riding cock", + "riding compilation", + "riding creampie", + "riding dick", + "riding dildo", + "riding porn", + "riley evans", + "riley mason", + "riley shy", + "rim job porn", + "rita faltoyano", + "robot porn", + "rocco siffredi", + "role playing porn", + "roleplay porn", + "roman porn", + "romance porn", + "romanian porn", + "romantic fuck", + "romantic porn", + "romantic sex", + "ron jeremy", + "rough anal", + "rough blowjob", + "rough deepthroat", + "rough fuck", + "rough gangbang", + "rough lesbian porn", + "rough lesbian sex", + "rough porn", + "rough sex", + "round ass", + "round tits", + "roxy deville", + "roxy jezel", + "roxy reynolds", + "rubbing pussy", + "ruined orgasm", + "russian anal", + "russian babes", + "russian fuck", + "russian fucking", + "russian lesbian", + "russian lesbians", + "russian mature", + "russian milf", + "russian mom", + "russian porn", + "russian porno", + "russian pornstars", + "russian pussy", + "russian sex", + "russian sluts", + "russian teen", + "russian threesome", + "rusty trombone", + "ryan conner", + "sabrine maui", + "sadie west", + "sahara knight", + "sakura sena", + "samantha sin", + "sammie rhodes", + "samoan porn", + "sandra romain", + "sandy sweet", + "sapphic erotica", + "sara jay", + "sara st", + "sara stone", + "sarah blake", + "sarah blue", + "sarah twain", + "sarah vandella", + "sasha grey", + "sasha knox", + "sativa rose", + "savannah gold", + "savannah stern", + "scarlett pain", + "scene porno", + "school girl", + "school girl porn", + "school girl sex", + "school porn", + "schoolgirl porn", + "schoolgirl sex", + "scouse porn", + "screw my wife", + "screw my wife please", + "sean michaels", + "self bondage", + "self fuck", + "self fucking", + "sensual porn", + "sensual sex", + "sex and submission", + "sex anime", + "sex arab", + "sex art", + "sex ass", + "sex bbw", + "sex black", + "sex cartoon", + "sex films", + "sex for cash", + "sex for free", + "sex free", + "sex free porn", + "sex free video", + "sex hot", + "sex korea", + "sex massage", + "sex massage video", + "sex moves", + "sex movies", + "sex movies free", + "sex party", + "sex porn", + "sex porn tube", + "sex porn video", + "sex porn videos", + "sex porno", + "sex porno video", + "sex pornos", + "sex positions", + "sex pron", + "sex public", + "sex pussy", + "sex streaming", + "sex tape", + "sex teacher", + "sex tubes", + "sex underwater", + "sex videos", + "sex videos free", + "sexy ass", + "sexy asses", + "sexy blonde", + "sexy fuck", + "sexy girl", + "sexy latina", + "sexy lesbians", + "sexy massage", + "sexy milf", + "sexy naked women", + "sexy older women", + "sexy porn", + "sexy secretary", + "sexy tits", + "sg4ge", + "sharing my wife", + "sharka blue", + "sharon wi", + "sharon wild", + "shaved cock", + "shaved pussy", + "shaving pussy", + "shawna lenee", + "sheila marie", + "short hair", + "show me your tits", + "shower porn", + "shower sex", + "shy love", + "shyla stylez", + "sienna west", + "sierra sin", + "sierra sinn", + "silvia saint", + "simone peach", + "simony diamond", + "sindee jennings", + "sinnamon love", + "sister and brother", + "skinny teen", + "skyy black", + "sleep porn", + "sleeping porn", + "sloppy blowjob", + "slow blowjob", + "slut porn", + "slut roulette", + "small dick", + "small dick porn", + "small penis porn", + "small pussy", + "small tits", + "smoking fetish", + "smoking porn", + "soapy massage", + "soft porn", + "soft porno", + "softcore porn", + "some ho", + "sondra hall", + "sonia red", + "sophia castello", + "sophie dee", + "sophie evans", + "sophie moone", + "spanish porn", + "spank porn", + "spiderman porn", + "spring break", + "squirt compilation", + "squirt porn", + "squirting dildo", + "squirting girls", + "squirting orgasm", + "squirting porn", + "squirting pussy", + "stacy silver", + "stacy valentine", + "step dad", + "step daughter", + "step mom", + "step sister", + "stephanie cane", + "stephanie swift", + "stepmom porn", + "stickam girls", + "stocking porn", + "stormy daniels", + "story porn", + "strap on", + "strapon cum", + "strapon porn", + "stream porn", + "stream sex", + "streaming porn", + "streaming porno", + "streaming sex", + "street porn", + "sucking cock", + "sunny lane", + "sunrise adams", + "super porno", + "superhero porn", + "surprise anal", + "suzie carina", + "swallow compilation", + "swedish porn", + "sweet porn", + "sweet pussy", + "sweet tee", + "swinger party", + "swinger porn", + "swingers party", + "swingers porn", + "sydnee capri", + "sydnee steele", + "tabitha stevens", + "taboo porn", + "tall girl porn", + "tango porno", + "tanya james", + "tarra white", + "taryn thomas", + "tarzan porn", + "tarzan sex", + "tasteful porn", + "tatoo porn", + "tattoo porn", + "tawny roberts", + "taylor rain", + "taylor st. cla", + "taylor st. claire", + "teacher fuck", + "teacher fucks student", + "teacher milf", + "teacher porn", + "teacher sex videos", + "teacher student porn", + "teagan presley", + "teanna kai", + "tease porn", + "teen amateur", + "teen anal", + "teen ass", + "teen big ass", + "teen big cock", + "teen big tits", + "teen blonde", + "teen blowjob", + "teen blowjobs", + "teen bondage", + "teen bukkake", + "teen casting", + "teen compilation", + "teen couple", + "teen creampie", + "teen cum", + "teen cumshot", + "teen cumshot compilation", + "teen deepthroat", + "teen double penetration", + "teen facials", + "teen first anal", + "teen fisting", + "teen fuck", + "teen gangbang", + "teen girl porn", + "teen lesbian", + "teen masturbation", + "teen pov", + "teen threesome", + "teen titans hentai", + "teen titans porn", + "teenage porn", + "teenage robot porn", + "teenager sex", + "tentacle hentai", + "tentacle porn", + "tentacle sex", + "tera patrick", + "terri summers", + "thai anal", + "thai creampie", + "thai hooker", + "thai porn", + "the avengers xxx", + "thick cock", + "thong porn", + "threesome creampie", + "threesome porn", + "threesome sex", + "throat cum", + "throat fuck", + "throbbing cock", + "tia ling", + "tia sweets", + "tia tanaka", + "tiana lynn", + "tied up and fucked", + "tied up porn", + "tiffany holiday", + "tiffany hopkins", + "tiffany mynx", + "tiffany preston", + "tiffany rayne", + "tiffany taylor", + "tight anal", + "tight asian pussy", + "tight ass", + "tight black pussy", + "tight cunt", + "tight pussy", + "tight pussy porn", + "tiny pussy", + "tiny teen", + "tiny tits", + "tit fuck", + "tit porn", + "tit torture", + "tit wank", + "titfuck compilation", + "tits and ass", + "titty fuck", + "titty fucking", + "tokyo hot", + "tomb raider xxx", + "tommy lee sex tape", + "tonights girlfriend", + "too big", + "toon porn", + "toon sex", + "top porn", + "tori black", + "torture porn", + "tory lane", + "toy porn", + "train sex", + "trina michaels", + "trinidad porn", + "trios porno", + "triple anal", + "triple blowjob", + "triple penetration", + "tron porn", + "truth or dare porn", + "tube adult", + "tube eight", + "tube japanese", + "tube movies", + "tube porn", + "tube porno", + "tube sex", + "tube sites", + "tube video", + "tube videos", + "tube8", + "turk sex", + "turkish porn", + "turkish sex", + "twerk porn", + "twerking naked", + "twin porn", + "twins porn", + "tyla wynn", + "tyra misoux", + "tyra moore", + "ugandan porn", + "ugly americans hentai", + "ugly girl", + "ugly lesbians", + "ugly porn", + "ugly pussy", + "ugly sluts", + "uk escort", + "uk escort porn", + "uk flashers", + "uk milf", + "uk porn", + "uk porn party", + "uk pornstars", + "uk sluts", + "uk swingers", + "ukraine porn", + "ukraine pussy", + "ukrainian porn", + "ukrainian pussy", + "ultimate blowjob", + "ultimate boobs", + "ultimate deepthroat", + "ultimate fuck toy", + "ultimate handjob", + "ultimate surrender", + "umemaro hentai", + "unbelievable deepthroat", + "uncensored anime", + "uncensored hentai", + "uncensored japanese", + "uncensored japanese porn", + "uncensored porn", + "uncircumcised porn", + "uncle porn", + "uncontrollable orgasm", + "uncut cock", + "uncut cocks", + "uncut dick", + "under table", + "underground porn", + "underwater blowjob", + "underwater creampie", + "underwater porn", + "underwater sex", + "underwear fetish", + "underwear porn", + "underworld porn", + "undressed yoga", + "undressing porn", + "unexpected anal", + "unexpected creampie", + "unexpected cumshot", + "unexpected orgasm", + "unexpected porn", + "unexpected sex", + "uniform porn", + "uniform sex", + "uniform xxx", + "university porn", + "unnatural sex", + "unreal boobs", + "unreal tits", + "untouched pussy", + "unused pussies", + "unusual porn", + "unwanted anal", + "unwanted creampie", + "unwanted cum", + "unwanted cumshot", + "unwanted facials", + "unwanted orgasm", + "up close pussy", + "up the ass", + "upper floor", + "upside down blowjob", + "upskirt ass", + "upskirt fuck", + "upskirt hd", + "upskirt no panties", + "upskirt porn", + "upskirt pussy", + "upskirt sexy", + "upskirt spy", + "upskirt video", + "upskirt voyeur", + "urban porn", + "urethral play", + "urinal cam", + "urinal spy", + "using a fleshlight", + "using a vibrator", + "vampire porn", + "vanessa blue", + "vanessa lane", + "vanessa leon", + "velicity von", + "veronica l", + "veronica rayne", + "veronica vanoza", + "veronique vega", + "very young", + "very young girl", + "vicky vett", + "vicky vette", + "victoria allure", + "victoria rose", + "victoria sin", + "victorian porn", + "victorious porn", + "vietnamese porn", + "vintage anal", + "vintage porn", + "vintage porno", + "vintage sex", + "violent porn", + "vip porno", + "virgin anal", + "virgin ass", + "virgin defloration", + "virgin first time", + "virgin fuck", + "virgin porn", + "virgin pussy", + "virgin sex", + "virgin teen", + "virgin teen porn", + "virginity porn", + "virtual blowjob", + "virtual porn", + "virtual sex", + "viv thomas", + "vivian schmitt", + "volleyball porn", + "voyeur amateur", + "voyeur beach", + "voyeur masturbation", + "voyeur porn", + "wake up sex", + "wasteland porn", + "watch my gf", + "watch my girlfriend", + "watching my wife fuck", + "watching porn", + "webcam masturbation", + "webcam porn", + "webcam teen", + "wedding orgy", + "wedding porn", + "weed porn", + "weird porn", + "welsh porn", + "werewolf porn", + "western porn", + "wet ass", + "wet black pussy", + "wet cunt", + "wet orgasm", + "wet panties", + "wet porn", + "wet pussy", + "wet tits", + "wetting her panties", + "whipped ass", + "white ass", + "white booty", + "white girl porn", + "white porn", + "white pussy", + "whitney stevens", + "wife anal", + "wife bbc", + "wife blowjob", + "wife breeding", + "wife bucket", + "wife cheating", + "wife crazy", + "wife creampie", + "wife dp", + "wife facial", + "wife flashing", + "wife friend", + "wife gangbang", + "wife handjob", + "wife orgasm", + "wife orgy", + "wife share", + "wife sharing", + "wife sucking cock", + "wife swap", + "wife threesome", + "wife watching", + "wife xxx", + "wifeys world", + "wild lesbian sex", + "wild porn", + "wild sex", + "woman friendly porn", + "women at work hentai", + "women friendly porn", + "women fucking", + "women masturbating", + "women pissing", + "women porn", + "women squirting", + "women sucking cock", + "wonder woman porn", + "woodman casting", + "workout porn", + "worlds biggest cumshot", + "wow girls", + "wrestling porn", + "wrestling sex", + "wrong hole", + "wwe porn", + "x mas", + "x-art", + "xl porn", + "xmas fuck", + "xmas party", + "x-men porn", + "xxl porn", + "xxx anal", + "xxx anime", + "xxx arab", + "xxx ass", + "xxx bbw", + "xxx big tits", + "xxx black", + "xxx blonde", + "xxx brazil", + "xxx cartoon", + "xxx casting", + "xxx extreme", + "xxx france", + "xxx girls", + "xxx hardcore", + "xxx hd", + "xxx hentai", + "xxx hindi", + "xxx latinas", + "xxx lesbian", + "xxx manga", + "xxx massage", + "xxx milf", + "xxx movies", + "xxx parody", + "xxx porn", + "xxx proposal", + "xxx pussy", + "xxx rated", + "xxx rated movies", + "xxx squirt", + "xxx streaming", + "xxx teen", + "xxx wife", + "yoga fuck", + "yoga pants", + "yoga porn", + "yoga sex", + "yorkshire porn", + "young amateur", + "young amateur couple", + "young amateur porn", + "young anal", + "young anal sex", + "young and old", + "young and old lesbians", + "young anime porn", + "young asian", + "young asian girls", + "young ass", + "young big tits", + "young black ass", + "young black lesbians", + "young black porn", + "young blonde", + "young blowjob", + "young boobs", + "young boy", + "young brunette", + "young chubby", + "young cock", + "young couple", + "young couple fucking", + "young couple sex", + "young creampie", + "young deepthroat", + "young ebony", + "young ebony porn", + "young fat pussy", + "young fatties", + "young fuck", + "young girl", + "young girl old man", + "young girls", + "young group sex", + "young handjob", + "young harlots", + "young hentai", + "young hot girls", + "young hotties", + "young indian porn", + "young japanese porn", + "young legal porn", + "young lesbian", + "young lesbian seduction", + "young masturbation", + "young milf", + "young naked women", + "young old", + "young old porn", + "young orgasm", + "young orgy", + "young porn", + "young pornstars", + "young puffy nipples", + "young pussy", + "young russian", + "young school girl porn", + "young sex", + "young sex parties", + "young sluts", + "young squirt", + "young swingers", + "young teen", + "young teens", + "young threesome", + "young throats", + "young tight pussy", + "young tits", + "young wet pussy", + "young women fucking", + "your porn", + "youtube adult", + "youtube sex video", + "youtube sex videos", + "yummy mama", + "yummy porn", + "yummy pussy", + "zelda porn", + "zelda porno", + "zimbabwe sex", + "zimbabwean porn", + "zombie fuck porn", + "zombie girl fucked", + "zombie porn", + "zombie porno", + "zumba xxx" + ] + } +} \ No newline at end of file diff --git a/src/producer/Extensions/ConfigurationExtensions.cs b/src/producer/Extensions/ConfigurationExtensions.cs index 903b73a..bdadb3d 100644 --- a/src/producer/Extensions/ConfigurationExtensions.cs +++ b/src/producer/Extensions/ConfigurationExtensions.cs @@ -12,7 +12,8 @@ public static class ConfigurationExtensions configuration.AddJsonFile(LoggingConfig, false, true); configuration.AddJsonFile(ScrapeConfiguration.Filename, false, true); configuration.AddJsonFile(TorrentioConfiguration.Filename, false, true); - + configuration.AddJsonFile(AdultContentConfiguration.Filename, false, true); + configuration.AddEnvironmentVariables(); configuration.AddUserSecrets(); diff --git a/src/producer/Features/Crawlers/Dmm/DebridMediaManagerCrawler.cs b/src/producer/Features/Crawlers/Dmm/DebridMediaManagerCrawler.cs index aba72c2..64badb8 100644 --- a/src/producer/Features/Crawlers/Dmm/DebridMediaManagerCrawler.cs +++ b/src/producer/Features/Crawlers/Dmm/DebridMediaManagerCrawler.cs @@ -4,13 +4,18 @@ public partial class DebridMediaManagerCrawler( IHttpClientFactory httpClientFactory, ILogger logger, IDataStorage storage, - GithubConfiguration githubConfiguration) : BaseCrawler(logger, storage) + GithubConfiguration githubConfiguration, + AdultContentConfiguration adultContentConfiguration, + IServiceProvider serviceProvider) : BaseCrawler(logger, storage) { [GeneratedRegex("""""")] private static partial Regex HashCollectionMatcher(); [GeneratedRegex(@"[sS]([0-9]{1,2})|seasons?[\s-]?([0-9]{1,2})", RegexOptions.IgnoreCase, "en-GB")] private static partial Regex SeasonMatcher(); + + [GeneratedRegex(@"[0-9]{4}", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex YearMatcher(); private const string DownloadBaseUrl = "https://raw.githubusercontent.com/debridmediamanager/hashlists/main"; @@ -18,8 +23,15 @@ public partial class DebridMediaManagerCrawler( protected override string Url => "https://api.github.com/repos/debridmediamanager/hashlists/git/trees/main?recursive=1"; protected override string Source => "DMM"; + private IFuzzySearcher? _adultContentSearcher; + public override async Task Execute() { + if (!adultContentConfiguration.Allow) + { + _adultContentSearcher = serviceProvider.GetRequiredService>(); + } + var client = httpClientFactory.CreateClient("Scraper"); client.DefaultRequestHeaders.Authorization = new("Bearer", githubConfiguration.PAT); client.DefaultRequestHeaders.UserAgent.ParseAdd("curl"); @@ -95,12 +107,20 @@ public partial class DebridMediaManagerCrawler( private Torrent? ParseTorrent(JsonElement item) { + + if (!item.TryGetProperty("filename", out var filenameElement) || + !item.TryGetProperty("bytes", out var bytesElement) || + !item.TryGetProperty("hash", out var hashElement)) + { + return null; + } + var torrent = new Torrent { Source = Source, - Name = item.GetProperty("filename").GetString(), - Size = item.GetProperty("bytes").GetInt64().ToString(), - InfoHash = item.GetProperty("hash").ToString(), + Name = filenameElement.GetString(), + Size = bytesElement.GetInt64().ToString(), + InfoHash = hashElement.ToString(), Seeders = 0, Leechers = 0, }; @@ -110,9 +130,39 @@ public partial class DebridMediaManagerCrawler( return null; } - torrent.Category = SeasonMatcher().IsMatch(torrent.Name) ? "tv" : "movies"; + torrent.Category = (SeasonMatcher().IsMatch(torrent.Name), YearMatcher().IsMatch(torrent.Name)) switch + { + (true, _) => "tv", + (_, true) => "movies", + _ => "unknown", + }; - return torrent; + return HandleAdultContent(torrent); + } + + private Torrent HandleAdultContent(Torrent torrent) + { + try + { + if (!adultContentConfiguration.Allow) + { + var adultMatch = _adultContentSearcher!.Search(torrent.Name.Replace(".", " ")); + + if (adultMatch.Count > 0) + { + logger.LogWarning("Adult content found in {Name}. Marking category as 'xxx'", torrent.Name); + logger.LogWarning("Matches: {TopMatch} {TopScore}", adultMatch.First().Value, adultMatch.First().Score); + torrent.Category = "xxx"; + } + } + + return torrent; + } + catch (Exception e) + { + logger.LogWarning("Failed to handle adult content for {Name}: [{Error}]. Torrent will not be ingested at this time.", torrent.Name, e.Message); + return null; + } } private async Task InsertTorrentsForPage(JsonDocument json) diff --git a/src/producer/Features/Crawlers/Dmm/ParsingService.Regex.cs b/src/producer/Features/Crawlers/Dmm/ParsingService.Regex.cs new file mode 100644 index 0000000..496699e --- /dev/null +++ b/src/producer/Features/Crawlers/Dmm/ParsingService.Regex.cs @@ -0,0 +1,29 @@ +namespace Producer.Features.Crawlers.Dmm; + +public partial class ParsingService +{ + [GeneratedRegex(@"[^a-z0-9]")] + private static partial Regex NakedMatcher(); + + [GeneratedRegex(@"\d{4}")] + private static partial Regex GrabYearsMatcher(); + + [GeneratedRegex(@"\d+")] + private static partial Regex GrabPossibleSeasonNumsMatcher(); + + [GeneratedRegex(@"(.)\1+")] + private static partial Regex RemoveRepeatsMatcher(); + + [GeneratedRegex(@"m{0,4}(cm|cd|d?c{0,3})(xc|xl|l?x{0,3})(ix|iv|v?i{0,3})")] + private static partial Regex ReplaceRomanWithDecimalMatcher(); + + [GeneratedRegex(@"\s+")] + private static partial Regex WhitespaceMatcher(); + + [GeneratedRegex(@"\W+")] + private static partial Regex WordMatcher(); + + + [GeneratedRegex(@"'s|\s&\s|\W")] + private static partial Regex WordProcessingMatcher(); +} \ No newline at end of file diff --git a/src/producer/Features/Crawlers/Dmm/ParsingService.cs b/src/producer/Features/Crawlers/Dmm/ParsingService.cs new file mode 100644 index 0000000..51a542d --- /dev/null +++ b/src/producer/Features/Crawlers/Dmm/ParsingService.cs @@ -0,0 +1,324 @@ +namespace Producer.Features.Crawlers.Dmm; + +public partial class ParsingService(AdultContentConfiguration adultContentConfiguration) +{ + private static readonly char[] WhitespaceSeparator = [' ']; + + //todo: Populate dictionary + private static readonly HashSet Dictionary = new HashSet(); + + public static string Naked(string title) => + NakedMatcher().Replace(title.ToLower(), ""); + + public static List GrabYears(string str) + { + var matches = GrabYearsMatcher().Matches(str); + return matches + .Select(m => m.Value) + .Where(n => int.Parse(n) > 1900 && int.Parse(n) <= DateTime.Now.Year) + .ToList(); + } + + public static List GrabPossibleSeasonNums(string str) + { + var matches = GrabPossibleSeasonNumsMatcher().Matches(str); + return matches + .Select(m => int.Parse(m.Value)) + .Where(n => n is > 0 and <= 500) + .ToList(); + } + + public static bool HasYear(string test, List years, bool strictCheck = false) => + strictCheck + ? years.Any(test.Contains) + : years.Any(year => + { + var intYear = int.Parse(year); + return test.Contains(year) || + test.Contains($"{intYear + 1}") || + test.Contains($"{intYear - 1}"); + }); + + public static string RemoveDiacritics(string str) + { + var normalizedString = str.Normalize(NormalizationForm.FormD); + var stringBuilder = new StringBuilder(); + + foreach (var c in normalizedString) + { + var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c); + if (unicodeCategory != UnicodeCategory.NonSpacingMark) + { + stringBuilder.Append(c); + } + } + + return stringBuilder.ToString().Normalize(NormalizationForm.FormC); + } + + public static string RemoveRepeats(string str) => RemoveRepeatsMatcher().Replace(str, "$1"); + + public static int RomanToDecimal(string roman) + { + var romanNumerals = new Dictionary + { + {'I', 1}, + {'V', 5}, + {'X', 10}, + {'L', 50}, + {'C', 100}, + {'D', 500}, + {'M', 1000} + }; + + var total = 0; + var prevValue = 0; + + for (var i = roman.Length - 1; i >= 0; i--) + { + var currentValue = romanNumerals[roman[i].ToString().ToUpper()[0]]; + total = currentValue < prevValue ? total - currentValue : total + currentValue; + prevValue = currentValue; + } + + return total; + } + + public static string ReplaceRomanWithDecimal(string input) => ReplaceRomanWithDecimalMatcher().Replace(input, match => RomanToDecimal(match.Value).ToString()); + + public static bool StrictEqual(string title1, string title2) + { + title1 = WhitespaceMatcher().Replace(title1, ""); + title2 = WhitespaceMatcher().Replace(title2, ""); + + return (title1.Length > 0 && title1 == title2) || + (Naked(title1).Length > 0 && Naked(title1) == Naked(title2)) || + (RemoveRepeats(title1).Length > 0 && RemoveRepeats(title1) == RemoveRepeats(title2)) || + (RemoveDiacritics(title1).Length > 0 && RemoveDiacritics(title1) == RemoveDiacritics(title2)); + } + + public static int CountTestTermsInTarget(string test, string target, bool shouldBeInSequence = false) + { + var replaceCount = 0; + var prevReplaceCount = 0; + var prevOffset = 0; + var prevLength = 0; + const int wordTolerance = 5; + + var wordsInTitle = WordMatcher().Split(target).Where(e => !string.IsNullOrEmpty(e)).ToList(); + const int magicLength = 3; + var testStr = test; + + var inSequenceTerms = 1; + var longestSequence = 0; + + MatchEvaluator replacer = match => + { + if (shouldBeInSequence && prevLength > 0 && match.Index >= wordTolerance) + { + if (inSequenceTerms > longestSequence) + { + longestSequence = inSequenceTerms; + } + + inSequenceTerms = 0; + } + prevOffset = match.Index; + prevLength = match.Length; + replaceCount++; + inSequenceTerms++; + return match.Value; + }; + + Action wrapReplace = (newTerm, first, last) => + { + var prefix = first ? @"\b" : ""; + var suffix = last ? @"\b" : ""; + testStr = Regex.Replace(testStr.Substring(prevOffset + prevLength), $"{prefix}{newTerm}{suffix}", replacer); + }; + + var actual = wordsInTitle.Where((term, idx) => + { + var first = idx == 0; + var last = idx == wordsInTitle.Count - 1; + testStr = testStr[(prevOffset + prevLength)..]; + wrapReplace(term, first, last); + if (replaceCount > prevReplaceCount) + { + prevReplaceCount = replaceCount; + return true; + } + if (RemoveDiacritics(term).Length >= magicLength) + { + wrapReplace(RemoveDiacritics(term), first, last); + if (replaceCount > prevReplaceCount) + { + prevReplaceCount = replaceCount; + return true; + } + } + if (RemoveRepeats(term).Length >= magicLength) + { + wrapReplace(RemoveRepeats(term), first, last); + if (replaceCount > prevReplaceCount) + { + prevReplaceCount = replaceCount; + return true; + } + } + if (Naked(term).Length >= magicLength) + { + wrapReplace(Naked(term), first, last); + if (replaceCount > prevReplaceCount) + { + prevReplaceCount = replaceCount; + return true; + } + } + + if (ReplaceRomanWithDecimal(term) == term) + { + return false; + } + + wrapReplace(ReplaceRomanWithDecimal(term), first, last); + + if (replaceCount <= prevReplaceCount) + { + return false; + } + + prevReplaceCount = replaceCount; + return true; + }).ToList(); + + if (shouldBeInSequence) + { + return inSequenceTerms > longestSequence ? inSequenceTerms : longestSequence; + } + return actual.Count; + } + + public static bool FlexEq(string test, string target, List years) + { + var movieTitle = TorrentTitleParser.Parse(test).Movie.Title.ToLower(); + var tvTitle = TorrentTitleParser.Parse(test, true).Show.Title.ToLower(); + + var target2 = WhitespaceMatcher().Replace(target, ""); + var test2 = WhitespaceMatcher().Replace(test, ""); + + var magicLength = HasYear(test, years) ? 3 : 5; + + if (Naked(target2).Length >= magicLength && test2.Contains(Naked(target2))) + { + return true; + } + + if (RemoveRepeats(target2).Length >= magicLength && test2.Contains(RemoveRepeats(target2))) + { + return true; + } + if (RemoveDiacritics(target2).Length >= magicLength && test2.Contains(RemoveDiacritics(target2))) + { + return true; + } + if (target2.Length >= Math.Ceiling(magicLength * 1.5) && test2.Contains(target2)) + { + return true; + } + + return StrictEqual(target, movieTitle) || StrictEqual(target, tvTitle); + } + + public static bool MatchesTitle(string target, List years, string test) + { + target = target.ToLower(); + test = test.ToLower(); + + var splits = WordMatcher().Split(target).Where(e => !string.IsNullOrEmpty(e)).ToList(); + var containsYear = HasYear(test, years); + + if (FlexEq(test, target, years)) + { + var sequenceCheck = CountTestTermsInTarget(test, string.Join(' ', splits), true); + return containsYear || sequenceCheck >= 0; + } + + var totalTerms = splits.Count; + if (totalTerms == 0 || (totalTerms <= 2 && !containsYear)) + { + return false; + } + + var keyTerms = splits.Where(s => (s.Length > 1 && !Dictionary.Contains(s)) || s.Length > 5).ToList(); + keyTerms.AddRange(target.Split(WhitespaceSeparator, StringSplitOptions.RemoveEmptyEntries).Where(e => e.Length > 2)); + var keySet = new HashSet(keyTerms); + var commonTerms = splits.Where(s => !keySet.Contains(s)).ToList(); + + var hasYearScore = totalTerms * 1.5; + var totalScore = keyTerms.Count * 2 + commonTerms.Count + hasYearScore; + + if (keyTerms.Count == 0 && totalTerms <= 2 && !containsYear) + { + return false; + } + + var foundKeyTerms = CountTestTermsInTarget(test, string.Join(' ', keyTerms)); + var foundCommonTerms = CountTestTermsInTarget(test, string.Join(' ', commonTerms)); + var score = foundKeyTerms * 2 + foundCommonTerms + (containsYear ? hasYearScore : 0); + + return Math.Floor(score / 0.85) >= totalScore; + } + + public static bool IncludesMustHaveTerms(List mustHaveTerms, string testTitle) => + mustHaveTerms.All(term => + { + var newTitle = testTitle.Replace(term, ""); + if (newTitle != testTitle) + { + testTitle = newTitle; + return true; + } + + newTitle = testTitle.Replace(RemoveDiacritics(term), ""); + if (newTitle != testTitle) + { + testTitle = newTitle; + return true; + } + + newTitle = testTitle.Replace(RemoveRepeats(term), ""); + if (newTitle != testTitle) + { + testTitle = newTitle; + return true; + } + + return false; + }); + + public bool HasNoBannedTerms(string targetTitle, string testTitle) + { + var words = WordMatcher().Split(testTitle.ToLower()).Where(word => word.Length > 3).ToList(); + + var hasBannedWords = words.Any(word => !targetTitle.Contains(word) && adultContentConfiguration.Keywords.Contains(word)); + + var titleWithoutSymbols = string.Join(' ', WordMatcher().Split(testTitle.ToLower())); + + var hasBannedCompoundWords = adultContentConfiguration.CompoundKeywords.Any(compoundWord => !targetTitle.Contains(compoundWord) && titleWithoutSymbols.Contains(compoundWord)); + + return !hasBannedWords && !hasBannedCompoundWords; + } + + public bool MeetsTitleConditions(string targetTitle, List years, string testTitle) => MatchesTitle(targetTitle, years, testTitle) && HasNoBannedTerms(targetTitle, testTitle); + + public static int CountUncommonWords(string title) + { + var processedTitle = WhitespaceMatcher().Split(title) + .Select(word => WordProcessingMatcher().Replace(word.ToLower(), "")) + .Where(word => word.Length > 3) + .ToList(); + + return processedTitle.Count(word => !Dictionary.Contains(word)); + } +} \ No newline at end of file diff --git a/src/producer/Features/Crawlers/Torrentio/TorrentioCrawler.cs b/src/producer/Features/Crawlers/Torrentio/TorrentioCrawler.cs index b14500b..fb7a386 100644 --- a/src/producer/Features/Crawlers/Torrentio/TorrentioCrawler.cs +++ b/src/producer/Features/Crawlers/Torrentio/TorrentioCrawler.cs @@ -190,7 +190,6 @@ public partial class TorrentioCrawler( Source = $"{Source}_{instance.Name}", InfoHash = infoHash, Category = "movies", // we only handle movies for now... - Imdb = imdbId, }; var span = title.AsSpan(); diff --git a/src/producer/Features/Crawlers/Tpb/TpbCrawler.cs b/src/producer/Features/Crawlers/Tpb/TpbCrawler.cs index 60a04d5..ead8c82 100644 --- a/src/producer/Features/Crawlers/Tpb/TpbCrawler.cs +++ b/src/producer/Features/Crawlers/Tpb/TpbCrawler.cs @@ -5,38 +5,43 @@ public class TpbCrawler(IHttpClientFactory httpClientFactory, ILogger "https://apibay.org/precompiled/data_top100_recent.json"; protected override string Source => "TPB"; - - // ReSharper disable once UnusedMember.Local - private readonly Dictionary> TpbCategories = new() - { - {"VIDEO", new() { - {"ALL", 200}, - {"MOVIES", 201}, - {"MOVIES_DVDR", 202}, - {"MUSIC_VIDEOS", 203}, - {"MOVIE_CLIPS", 204}, - {"TV_SHOWS", 205}, - {"HANDHELD", 206}, - {"MOVIES_HD", 207}, - {"TV_SHOWS_HD", 208}, - {"MOVIES_3D", 209}, - {"OTHER", 299}, - }}, - {"PORN", new() { - {"ALL", 500}, - {"MOVIES", 501}, - {"MOVIES_DVDR", 502}, - {"PICTURES", 503}, - {"GAMES", 504}, - {"MOVIES_HD", 505}, - {"MOVIE_CLIPS", 506}, - {"OTHER", 599}, - }}, - }; - + + // // ReSharper disable once UnusedMember.Local + // private readonly Dictionary> TpbCategories = new() + // { + // { + // "VIDEO", new() + // { + // {"ALL", 200}, + // {"MOVIES", 201}, + // {"MOVIES_DVDR", 202}, + // {"MUSIC_VIDEOS", 203}, + // {"MOVIE_CLIPS", 204}, + // {"TV_SHOWS", 205}, + // {"HANDHELD", 206}, + // {"MOVIES_HD", 207}, + // {"TV_SHOWS_HD", 208}, + // {"MOVIES_3D", 209}, + // {"OTHER", 299}, + // } + // }, + // { + // "PORN", new() + // { + // {"ALL", 500}, + // {"MOVIES", 501}, + // {"MOVIES_DVDR", 502}, + // {"PICTURES", 503}, + // {"GAMES", 504}, + // {"MOVIES_HD", 505}, + // {"MOVIE_CLIPS", 506}, + // {"OTHER", 599}, + // } + // }, + // }; + private static readonly HashSet TvSeriesCategories = [ 205, 208 ]; private static readonly HashSet MovieCategories = [ 201, 202, 207, 209 ]; - private static readonly HashSet PornCategories = [ 500, 501, 502, 505, 506 ]; private static readonly HashSet AllowedCategories = [ ..MovieCategories, ..TvSeriesCategories ]; protected override IReadOnlyDictionary Mappings @@ -47,7 +52,6 @@ public class TpbCrawler(IHttpClientFactory httpClientFactory, ILogger { - RegisterAutomaticRegistrationJobs(jobTypes, openMethod, quartz, scrapeConfiguration); + //RegisterAutomaticRegistrationJobs(jobTypes, openMethod, quartz, scrapeConfiguration); RegisterDmmJob(githubConfiguration, quartz, scrapeConfiguration); - RegisterTorrentioJob(services, quartz, configuration, scrapeConfiguration); - RegisterPublisher(quartz, rabbitConfiguration); + //RegisterTorrentioJob(services, quartz, configuration, scrapeConfiguration); + //RegisterPublisher(quartz, rabbitConfiguration); }); services.AddQuartzHostedService( diff --git a/src/producer/Features/ParseTorrentTitle/AudioChannels.cs b/src/producer/Features/ParseTorrentTitle/AudioChannels.cs new file mode 100644 index 0000000..8a9a9c9 --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/AudioChannels.cs @@ -0,0 +1,13 @@ +namespace Producer.Features.ParseTorrentTitle; + +public sealed class AudioChannels : SmartEnum +{ + public static readonly AudioChannels SEVEN = new("SEVEN", "7.1"); + public static readonly AudioChannels SIX = new("SIX", "5.1"); + public static readonly AudioChannels STEREO = new("STEREO", "stereo"); + public static readonly AudioChannels MONO = new ("MONO", "mono"); + + private AudioChannels(string name, string value) : base(name, value) + { + } +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/AudioChannelsParser.cs b/src/producer/Features/ParseTorrentTitle/AudioChannelsParser.cs new file mode 100644 index 0000000..8299f0f --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/AudioChannelsParser.cs @@ -0,0 +1,50 @@ +namespace Producer.Features.ParseTorrentTitle; + +public static partial class AudioChannelsParser +{ + [GeneratedRegex(@"\b(?7.?[01])\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex EightChannelExp(); + [GeneratedRegex(@"\b(?(6[\W]0(?:ch)?)(?=[^\d]|$)|(5[\W][01](?:ch)?)(?=[^\d]|$)|5ch|6ch)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex SixChannelExp(); + [GeneratedRegex(@"(?((2[\W]0(?:ch)?)(?=[^\d]|$))|(stereo))", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex StereoChannelExp(); + [GeneratedRegex(@"(?(1[\W]0(?:ch)?)(?=[^\d]|$)|(mono)|(1ch))", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex MonoChannelExp(); + + private static readonly Regex ChannelExp = new(string.Join("|", EightChannelExp(), SixChannelExp(), StereoChannelExp(), MonoChannelExp()), RegexOptions.IgnoreCase); + + public static void Parse(string title, out AudioChannels? channels, out string? source) + { + channels = null; + source = null; + + var channelResult = ChannelExp.Match(title); + if (!channelResult.Success) + { + return; + } + + var groups = channelResult.Groups; + + if (groups["eight"].Success) + { + channels = AudioChannels.SEVEN; + source = groups["eight"].Value; + } + else if (groups["six"].Success) + { + channels = AudioChannels.SIX; + source = groups["six"].Value; + } + else if (groups["stereo"].Success) + { + channels = AudioChannels.STEREO; + source = groups["stereo"].Value; + } + else if (groups["mono"].Success) + { + channels = AudioChannels.MONO; + source = groups["mono"].Value; + } + } +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/AudioCodecs.cs b/src/producer/Features/ParseTorrentTitle/AudioCodecs.cs new file mode 100644 index 0000000..0cc7433 --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/AudioCodecs.cs @@ -0,0 +1,22 @@ +namespace Producer.Features.ParseTorrentTitle; + +public sealed class AudioCodec : SmartEnum +{ + public static readonly AudioCodec MP3 = new("MP3", "MP3"); + public static readonly AudioCodec MP2 = new("MP2", "MP2"); + public static readonly AudioCodec DOLBY = new("DOLBY", "Dolby Digital"); + public static readonly AudioCodec EAC3 = new("EAC3", "Dolby Digital Plus"); + public static readonly AudioCodec AAC = new("AAC", "AAC"); + public static readonly AudioCodec FLAC = new("FLAC", "FLAC"); + public static readonly AudioCodec DTS = new("DTS", "DTS"); + public static readonly AudioCodec DTSHD = new("DTSHD", "DTS-HD"); + public static readonly AudioCodec TRUEHD = new("TRUEHD", "Dolby TrueHD"); + public static readonly AudioCodec OPUS = new("OPUS", "Opus"); + public static readonly AudioCodec VORBIS = new("VORBIS", "Vorbis"); + public static readonly AudioCodec PCM = new("PCM", "PCM"); + public static readonly AudioCodec LPCM = new("LPCM", "LPCM"); + + private AudioCodec(string name, string value) : base(name, value) + { + } +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/AudioCodecsParser.cs b/src/producer/Features/ParseTorrentTitle/AudioCodecsParser.cs new file mode 100644 index 0000000..42dd37d --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/AudioCodecsParser.cs @@ -0,0 +1,138 @@ +namespace Producer.Features.ParseTorrentTitle; + +public static partial class AudioCodecsParser +{ + [GeneratedRegex(@"\b(?(LAME(?:\d)+-?(?:\d)+)|(mp3))\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex Mp3CodecExp(); + + [GeneratedRegex(@"\b(?(mp2))\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex Mp2CodecExp(); + + [GeneratedRegex(@"\b(?(Dolby)|(Dolby-?Digital)|(DD)|(AC3D?))\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex DolbyCodecExp(); + + [GeneratedRegex(@"\b(?(Dolby-?Atmos))\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex DolbyAtmosCodecExp(); + + [GeneratedRegex(@"\b(?(AAC))(\d?.?\d?)(ch)?\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex AacAtmosCodecExp(); + + [GeneratedRegex(@"\b(?(EAC3|DDP|DD\+))\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex Eac3CodecExp(); + + [GeneratedRegex(@"\b(?(FLAC))\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex FlacCodecExp(); + + [GeneratedRegex(@"\b(?(DTS))\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex DtsCodecExp(); + + [GeneratedRegex(@"\b(?(DTS-?HD)|(DTS(?=-?MA)|(DTS-X)))\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex DtsHdCodecExp(); + + [GeneratedRegex(@"\b(?(True-?HD))\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex TrueHdCodecExp(); + + [GeneratedRegex(@"\b(?(Opus))\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex OpusCodecExp(); + + [GeneratedRegex(@"\b(?(Vorbis))\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex VorbisCodecExp(); + + [GeneratedRegex(@"\b(?(PCM))\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex PcmCodecExp(); + + [GeneratedRegex(@"\b(?(LPCM))\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex LpcmCodecExp(); + + private static readonly Regex AudioCodecExp = new( + string.Join( + "|", Mp3CodecExp(), Mp2CodecExp(), DolbyCodecExp(), DolbyAtmosCodecExp(), AacAtmosCodecExp(), Eac3CodecExp(), FlacCodecExp(), + DtsHdCodecExp(), + DtsCodecExp(), TrueHdCodecExp(), OpusCodecExp(), VorbisCodecExp(), PcmCodecExp(), LpcmCodecExp()), RegexOptions.IgnoreCase); + + public static void Parse(string title, out AudioCodec? codec, out string? source) + { + codec = null; + source = null; + + var audioResult = AudioCodecExp.Match(title); + + if (!audioResult.Success) + { + return; + } + + var groups = audioResult.Groups; + + if (groups["aac"].Success) + { + codec = AudioCodec.AAC; + source = groups["aac"].Value; + } + else if (groups["dolbyatmos"].Success) + { + codec = AudioCodec.EAC3; + source = groups["dolbyatmos"].Value; + } + else if (groups["dolby"].Success) + { + codec = AudioCodec.DOLBY; + source = groups["dolby"].Value; + } + else if (groups["dtshd"].Success) + { + codec = AudioCodec.DTSHD; + source = groups["dtshd"].Value; + } + else if (groups["dts"].Success) + { + codec = AudioCodec.DTS; + source = groups["dts"].Value; + } + else if (groups["flac"].Success) + { + codec = AudioCodec.FLAC; + source = groups["flac"].Value; + } + else if (groups["truehd"].Success) + { + codec = AudioCodec.TRUEHD; + source = groups["truehd"].Value; + } + else if (groups["mp3"].Success) + { + codec = AudioCodec.MP3; + source = groups["mp3"].Value; + } + else if (groups["mp2"].Success) + { + codec = AudioCodec.MP2; + source = groups["mp2"].Value; + } + else if (groups["pcm"].Success) + { + codec = AudioCodec.PCM; + source = groups["pcm"].Value; + } + else if (groups["lpcm"].Success) + { + codec = AudioCodec.LPCM; + source = groups["lpcm"].Value; + } + else if (groups["opus"].Success) + { + codec = AudioCodec.OPUS; + source = groups["opus"].Value; + } + else if (groups["vorbis"].Success) + { + codec = AudioCodec.VORBIS; + source = groups["vorbis"].Value; + } + else if (groups["eac3"].Success) + { + codec = AudioCodec.EAC3; + source = groups["eac3"].Value; + } + } +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/BaseParsed.cs b/src/producer/Features/ParseTorrentTitle/BaseParsed.cs new file mode 100644 index 0000000..2d68a9d --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/BaseParsed.cs @@ -0,0 +1,18 @@ +namespace Producer.Features.ParseTorrentTitle; + +public class BaseParsed +{ + public string? Title { get; set; } + public string? Year { get; set; } + public Edition? Edition { get; set; } + public Resolution? Resolution { get; set; } + public VideoCodec? VideoCodec { get; set; } + public AudioCodec? AudioCodec { get; set; } + public AudioChannels? AudioChannels { get; set; } + public Revision? Revision { get; set; } + public string? Group { get; set; } + public List Languages { get; set; } = []; + public List Sources { get; set; } = []; + public bool? Multi { get; set; } + public bool? Complete { get; set; } +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/Complete.cs b/src/producer/Features/ParseTorrentTitle/Complete.cs new file mode 100644 index 0000000..7fb97d6 --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/Complete.cs @@ -0,0 +1,14 @@ +namespace Producer.Features.ParseTorrentTitle; + +public static partial class Complete +{ + [GeneratedRegex(@"\b(NTSC|PAL)?.DVDR\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex CompleteDvdExp(); + + [GeneratedRegex(@"\b(COMPLETE)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex CompleteExp(); + public static bool? IsCompleteDvd(string title) => CompleteDvdExp().IsMatch(title) ? true : null; + + public static bool IsComplete(string title) => CompleteExp().IsMatch(title) || IsCompleteDvd(title) == true; + +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/Edition.cs b/src/producer/Features/ParseTorrentTitle/Edition.cs new file mode 100644 index 0000000..410a6c6 --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/Edition.cs @@ -0,0 +1,26 @@ +namespace Producer.Features.ParseTorrentTitle; + +public class Edition +{ + public bool? Internal { get; set; } + public bool? Limited { get; set; } + public bool? Remastered { get; set; } + public bool? Extended { get; set; } + public bool? Theatrical { get; set; } + public bool? Directors { get; set; } + public bool? Unrated { get; set; } + public bool? Imax { get; set; } + public bool? FanEdit { get; set; } + public bool? Hdr { get; set; } + public bool? Bw { get; set; } + public bool? ThreeD { get; set; } + public bool? Hsbs { get; set; } + public bool? Sbs { get; set; } + public bool? Hou { get; set; } + public bool? Uhd { get; set; } + public bool? Oar { get; set; } + public bool? DolbyVision { get; set; } + public bool? HardcodedSubs { get; set; } + public bool? DeletedScenes { get; set; } + public bool? BonusContent { get; set; } +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/EditionParser.cs b/src/producer/Features/ParseTorrentTitle/EditionParser.cs new file mode 100644 index 0000000..7f0e4fc --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/EditionParser.cs @@ -0,0 +1,101 @@ +namespace Producer.Features.ParseTorrentTitle; + +public static partial class EditionParser +{ + [GeneratedRegex(@"\b(INTERNAL)\b", RegexOptions.IgnoreCase)] + private static partial Regex InternalExp(); + + [GeneratedRegex(@"\b(Remastered|Anniversary|Restored)\b", RegexOptions.IgnoreCase)] + private static partial Regex RemasteredExp(); + + [GeneratedRegex(@"\b(IMAX)\b", RegexOptions.IgnoreCase)] + private static partial Regex ImaxExp(); + + [GeneratedRegex(@"\b(Uncensored|Unrated)\b", RegexOptions.IgnoreCase)] + private static partial Regex UnratedExp(); + + [GeneratedRegex(@"\b(Extended|Uncut|Ultimate|Rogue|Collector)\b", RegexOptions.IgnoreCase)] + private static partial Regex ExtendedExp(); + + [GeneratedRegex(@"\b(Theatrical)\b", RegexOptions.IgnoreCase)] + private static partial Regex TheatricalExp(); + + [GeneratedRegex(@"\b(Directors?)\b", RegexOptions.IgnoreCase)] + private static partial Regex DirectorsExp(); + + [GeneratedRegex(@"\b(Despecialized|Fan.?Edit)\b", RegexOptions.IgnoreCase)] + private static partial Regex FanExp(); + + [GeneratedRegex(@"\b(LIMITED)\b", RegexOptions.IgnoreCase)] + private static partial Regex LimitedExp(); + + [GeneratedRegex(@"\b(HDR)\b", RegexOptions.IgnoreCase)] + private static partial Regex HdrExp(); + + [GeneratedRegex(@"\b(3D)\b", RegexOptions.IgnoreCase)] + private static partial Regex ThreeD(); + + [GeneratedRegex(@"\b(Half-?SBS|HSBS)\b", RegexOptions.IgnoreCase)] + private static partial Regex Hsbs(); + + [GeneratedRegex(@"\b((?(\w+(?(HC|SUBBED)))\b", RegexOptions.IgnoreCase)] + private static partial Regex HardcodedSubsExp(); + + [GeneratedRegex(@"\b((Bonus.)?Deleted.Scenes)\b", RegexOptions.IgnoreCase)] + private static partial Regex DeletedScenes(); + + [GeneratedRegex(@"\b((Bonus|Extras|Behind.the.Scenes|Making.of|Interviews|Featurettes|Outtakes|Bloopers|Gag.Reel).(?!(Deleted.Scenes)))\b", RegexOptions.IgnoreCase)] + private static partial Regex BonusContent(); + + [GeneratedRegex(@"\b(BW)\b", RegexOptions.IgnoreCase)] + private static partial Regex Bw(); + + public static Edition Parse(string title) + { + TitleParser.Parse(title, out var parsedTitle, out _); + + var withoutTitle = title.Replace(".", " ").Replace(parsedTitle, "").ToLower(); + + var result = new Edition + { + Internal = InternalExp().IsMatch(withoutTitle), + Limited = LimitedExp().IsMatch(withoutTitle), + Remastered = RemasteredExp().IsMatch(withoutTitle), + Extended = ExtendedExp().IsMatch(withoutTitle), + Theatrical = TheatricalExp().IsMatch(withoutTitle), + Directors = DirectorsExp().IsMatch(withoutTitle), + Unrated = UnratedExp().IsMatch(withoutTitle), + Imax = ImaxExp().IsMatch(withoutTitle), + FanEdit = FanExp().IsMatch(withoutTitle), + Hdr = HdrExp().IsMatch(withoutTitle), + ThreeD = ThreeD().IsMatch(withoutTitle), + Hsbs = Hsbs().IsMatch(withoutTitle), + Sbs = Sbs().IsMatch(withoutTitle), + Hou = Hou().IsMatch(withoutTitle), + Uhd = Uhd().IsMatch(withoutTitle), + Oar = Oar().IsMatch(withoutTitle), + DolbyVision = DolbyVision().IsMatch(withoutTitle), + HardcodedSubs = HardcodedSubsExp().IsMatch(withoutTitle), + DeletedScenes = DeletedScenes().IsMatch(withoutTitle), + BonusContent = BonusContent().IsMatch(withoutTitle), + Bw = Bw().IsMatch(withoutTitle), + }; + + return result; + } +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/FileExtensionParser.cs b/src/producer/Features/ParseTorrentTitle/FileExtensionParser.cs new file mode 100644 index 0000000..37d3cd3 --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/FileExtensionParser.cs @@ -0,0 +1,78 @@ +namespace Producer.Features.ParseTorrentTitle; + +public static partial class FileExtensionParser +{ + [GeneratedRegex(@"\.[a-z0-9]{2,4}$", RegexOptions.IgnoreCase)] + private static partial Regex FileExtensionExp(); + + private static readonly List _fileExtensions = new() + { + // Unknown + ".webm", + // SDTV + ".m4v", + ".3gp", + ".nsv", + ".ty", + ".strm", + ".rm", + ".rmvb", + ".m3u", + ".ifo", + ".mov", + ".qt", + ".divx", + ".xvid", + ".bivx", + ".nrg", + ".pva", + ".wmv", + ".asf", + ".asx", + ".ogm", + ".ogv", + ".m2v", + ".avi", + ".bin", + ".dat", + ".dvr-ms", + ".mpg", + ".mpeg", + ".mp4", + ".avc", + ".vp3", + ".svq3", + ".nuv", + ".viv", + ".dv", + ".fli", + ".flv", + ".wpl", + + // DVD + ".img", + ".iso", + ".vob", + + // HD + ".mkv", + ".mk3d", + ".ts", + ".wtv", + + // Bluray + ".m2ts", + }; + + public static string RemoveFileExtension(string title) => + FileExtensionExp().Replace( + title, match => + { + if (_fileExtensions.Any(ext => ext.Equals(match.Value, StringComparison.OrdinalIgnoreCase))) + { + return string.Empty; + } + + return match.Value; + }); +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/GroupParser.cs b/src/producer/Features/ParseTorrentTitle/GroupParser.cs new file mode 100644 index 0000000..7fc4dac --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/GroupParser.cs @@ -0,0 +1,69 @@ +namespace Producer.Features.ParseTorrentTitle; + +public static partial class GroupParser +{ + [GeneratedRegex(@"^\[\s*[a-z]+(\.[a-z]+)+\s*\][- ]*|^www\.[a-z]+\.(?:com|net)[ -]*", RegexOptions.IgnoreCase)] + private static partial Regex WebsitePrefixExp(); + + [GeneratedRegex(@"(-(RP|1|NZBGeek|Obfuscated|Obfuscation|Scrambled|sample|Pre|postbot|xpost|Rakuv[a-z0-9]*|WhiteRev|BUYMORE|AsRequested|AlternativeToRequested|GEROV|Z0iDS3N|Chamele0n|4P|4Planet|AlteZachen|RePACKPOST))+$", RegexOptions.IgnoreCase)] + private static partial Regex CleanReleaseGroupExp(); + + [GeneratedRegex(@"-(?[a-z0-9]+)(?(?!\s).+?(?(Joy|YIFY|YTS.(MX|LT|AG)|FreetheFish|VH-PROD|FTW-HS|DX-TV|Blu-bits|afm72|Anna|Bandi|Ghost|Kappa|MONOLITH|Qman|RZeroX|SAMPA|Silence|theincognito|D-Z0N3|t3nzin|Vyndros|HDO|DusIctv|DHD|SEV|CtrlHD|-ZR-|ADC|XZVN|RH|Kametsu|r00t|HONE))(\])?$", RegexOptions.IgnoreCase)] + private static partial Regex ExceptionReleaseGroupRegex(); + + public static string? Parse(string title) + { + var nowebsiteTitle = WebsitePrefixExp().Replace(title, ""); + TitleParser.Parse(nowebsiteTitle, out var releaseTitle, out _); + releaseTitle = releaseTitle.Replace(" ", "."); + + var trimmed = nowebsiteTitle + .Replace(" ", ".") + .Replace(releaseTitle == nowebsiteTitle ? "" : releaseTitle, "") + .Replace(".-.", "."); + + trimmed = TitleParser.SimplifyTitle(FileExtensionParser.RemoveFileExtension(trimmed.Trim())); + + if (trimmed.Length == 0) + { + return null; + } + + var exceptionResult = ExceptionReleaseGroupRegex().Match(trimmed); + + if (exceptionResult.Groups["releasegroup"].Success) + { + return exceptionResult.Groups["releasegroup"].Value; + } + + var animeResult = AnimeReleaseGroupExp().Match(trimmed); + + if (animeResult.Success) + { + return animeResult.Groups["subgroup"].Value; + } + + trimmed = CleanReleaseGroupExp().Replace(trimmed, ""); + + var globalReleaseGroupExp = new Regex(ReleaseGroupRegexExp().ToString(), RegexOptions.IgnoreCase); + var result = globalReleaseGroupExp.Match(trimmed); + + while (result.Success) + { + if (result.Groups["releasegroup"].Success) + { + return result.Groups["releasegroup"].Value; + } + + result = result.NextMatch(); + } + + return null; + } +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/Language.cs b/src/producer/Features/ParseTorrentTitle/Language.cs new file mode 100644 index 0000000..4de54da --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/Language.cs @@ -0,0 +1,50 @@ +namespace Producer.Features.ParseTorrentTitle; + +public sealed class Language : SmartEnum +{ + public static readonly Language English = new("English", "English"); + public static readonly Language French = new("French", "French"); + public static readonly Language Spanish = new("Spanish", "Spanish"); + public static readonly Language German = new("German", "German"); + public static readonly Language Italian = new("Italian", "Italian"); + public static readonly Language Danish = new("Danish", "Danish"); + public static readonly Language Dutch = new("Dutch", "Dutch"); + public static readonly Language Japanese = new("Japanese", "Japanese"); + public static readonly Language Cantonese = new("Cantonese", "Cantonese"); + public static readonly Language Mandarin = new("Mandarin", "Mandarin"); + public static readonly Language Russian = new("Russian", "Russian"); + public static readonly Language Polish = new("Polish", "Polish"); + public static readonly Language Vietnamese = new("Vietnamese", "Vietnamese"); + public static readonly Language Nordic = new("Nordic", "Nordic"); + public static readonly Language Swedish = new("Swedish", "Swedish"); + public static readonly Language Norwegian = new("Norwegian", "Norwegian"); + public static readonly Language Finnish = new("Finnish", "Finnish"); + public static readonly Language Turkish = new("Turkish", "Turkish"); + public static readonly Language Portuguese = new("Portuguese", "Portuguese"); + public static readonly Language Flemish = new("Flemish", "Flemish"); + public static readonly Language Greek = new("Greek", "Greek"); + public static readonly Language Korean = new("Korean", "Korean"); + public static readonly Language Hungarian = new("Hungarian", "Hungarian"); + public static readonly Language Persian = new("Persian", "Persian"); + public static readonly Language Bengali = new("Bengali", "Bengali"); + public static readonly Language Bulgarian = new("Bulgarian", "Bulgarian"); + public static readonly Language Brazilian = new("Brazilian", "Brazilian"); + public static readonly Language Hebrew = new("Hebrew", "Hebrew"); + public static readonly Language Czech = new("Czech", "Czech"); + public static readonly Language Ukrainian = new("Ukrainian", "Ukrainian"); + public static readonly Language Catalan = new("Catalan", "Catalan"); + public static readonly Language Chinese = new("Chinese", "Chinese"); + public static readonly Language Thai = new("Thai", "Thai"); + public static readonly Language Hindi = new("Hindi", "Hindi"); + public static readonly Language Tamil = new("Tamil", "Tamil"); + public static readonly Language Arabic = new("Arabic", "Arabic"); + public static readonly Language Estonian = new("Estonian", "Estonian"); + public static readonly Language Icelandic = new("Icelandic", "Icelandic"); + public static readonly Language Latvian = new("Latvian", "Latvian"); + public static readonly Language Lithuanian = new("Lithuanian", "Lithuanian"); + public static readonly Language Romanian = new("Romanian", "Romanian"); + public static readonly Language Slovak = new("Slovak", "Slovak"); + public static readonly Language Serbian = new("Serbian", "Serbian"); + + private Language(string name, string value) : base(name, value) { } +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/LanguageParser.cs b/src/producer/Features/ParseTorrentTitle/LanguageParser.cs new file mode 100644 index 0000000..7863f2a --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/LanguageParser.cs @@ -0,0 +1,340 @@ +namespace Producer.Features.ParseTorrentTitle; + +public static partial class LanguageParser +{ + [GeneratedRegex(@"\bWEB-?DL\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex WebDL(); + + [GeneratedRegex(@"(?\bukr\b)", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex UkrainianRegex(); + + [GeneratedRegex(@"\b(PL|PLDUB|POLISH)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex PolishRegex(); + + [GeneratedRegex(@"\b(nl|dutch)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex DutchRegex(); + + [GeneratedRegex(@"\b(HIN|Hindi)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex HindiRegex(); + + [GeneratedRegex(@"\b(TAM|Tamil)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex TamilRegex(); + + [GeneratedRegex(@"\b(Arabic)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex ArabicRegex(); + + [GeneratedRegex(@"\b(Latvian)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex LatvianRegex(); + + [GeneratedRegex(@"\b(Lithuanian)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex LithuanianRegex(); + + [GeneratedRegex(@"\b(RO|Romanian|rodubbed)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex RomanianRegex(); + + [GeneratedRegex(@"\b(SK|Slovak)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex SlovakRegex(); + + [GeneratedRegex(@"\b(Brazilian)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex BrazilianRegex(); + + [GeneratedRegex(@"\b(Persian)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex PersianRegex(); + + [GeneratedRegex(@"\b(Bengali)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex BengaliRegex(); + + [GeneratedRegex(@"\b(Bulgarian)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex BulgarianRegex(); + + [GeneratedRegex(@"\b(Serbian)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex SerbianRegex(); + + public static void Parse(string title, out List languages) + { + TitleParser.Parse(title, out var parsedTitle, out _); + + var languageTitle = title.Replace(".", " ").Replace(parsedTitle, "").ToLower(); + + languages = new(); + + if (languageTitle.Contains("spanish")) + { + languages.Add(Language.Spanish); + } + + if (languageTitle.Contains("japanese")) + { + languages.Add(Language.Japanese); + } + + if (languageTitle.Contains("cantonese")) + { + languages.Add(Language.Cantonese); + } + + if (languageTitle.Contains("mandarin")) + { + languages.Add(Language.Mandarin); + } + + if (languageTitle.Contains("korean")) + { + languages.Add(Language.Korean); + } + + if (languageTitle.Contains("vietnamese")) + { + languages.Add(Language.Vietnamese); + } + + if (languageTitle.Contains("finnish")) + { + languages.Add(Language.Finnish); + } + + if (languageTitle.Contains("turkish")) + { + languages.Add(Language.Turkish); + } + + if (languageTitle.Contains("portuguese")) + { + languages.Add(Language.Portuguese); + } + + if (languageTitle.Contains("hebrew")) + { + languages.Add(Language.Hebrew); + } + + if (languageTitle.Contains("czech")) + { + languages.Add(Language.Czech); + } + + if (languageTitle.Contains("ukrainian")) + { + languages.Add(Language.Ukrainian); + } + + if (languageTitle.Contains("catalan")) + { + languages.Add(Language.Catalan); + } + + if (languageTitle.Contains("estonian")) + { + languages.Add(Language.Estonian); + } + + if (languageTitle.Contains("thai")) + { + languages.Add(Language.Thai); + } + + if (EnglishRegex().IsMatch(languageTitle)) + { + languages.Add(Language.English); + } + + if (DanishRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Danish); + } + + if (SwedishRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Swedish); + } + + if (IcelandicRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Icelandic); + } + + if (ChineseRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Chinese); + } + + if (ItalianRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Italian); + } + + if (GermanRegex().IsMatch(languageTitle)) + { + languages.Add(Language.German); + } + + if (FlemishRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Flemish); + } + + if (GreekRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Greek); + } + + if (FrenchRegex().IsMatch(languageTitle)) + { + languages.Add(Language.French); + } + + if (RussianRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Russian); + } + + if (NorwegianRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Norwegian); + } + + if (HungarianRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Hungarian); + } + + if (HebrewRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Hebrew); + } + + if (CzechRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Czech); + } + + if (UkrainianRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Ukrainian); + } + + if (PolishRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Polish); + } + + if (DutchRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Dutch); + } + + if (HindiRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Hindi); + } + + if (TamilRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Tamil); + } + + if (ArabicRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Arabic); + } + + if (LatvianRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Latvian); + } + + if (LithuanianRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Lithuanian); + } + + if (RomanianRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Romanian); + } + + if (SlovakRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Slovak); + } + + if (BrazilianRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Brazilian); + } + + if (PersianRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Persian); + } + + if (BengaliRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Bengali); + } + + if (BulgarianRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Bulgarian); + } + + if (SerbianRegex().IsMatch(languageTitle)) + { + languages.Add(Language.Serbian); + } + } + + public static bool? IsMulti(string title) + { + var noWebTitle = WebDL().Replace(title, ""); + return MultiExp().IsMatch(noWebTitle) ? true : null; + } +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/ParsedFilename.cs b/src/producer/Features/ParseTorrentTitle/ParsedFilename.cs new file mode 100644 index 0000000..f77dc41 --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/ParsedFilename.cs @@ -0,0 +1,11 @@ +namespace Producer.Features.ParseTorrentTitle; + +public class ParsedFilename +{ + public ParsedMovie? Movie { get; set; } + public ParsedTv? Show { get; set; } + public bool IsMovie => Movie is not null; + public bool IsShow => Show is not null; + + public bool IsInvalid => (!IsMovie && !IsShow) || (IsMovie && IsShow); +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/ParsedMovie.cs b/src/producer/Features/ParseTorrentTitle/ParsedMovie.cs new file mode 100644 index 0000000..5db6195 --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/ParsedMovie.cs @@ -0,0 +1,5 @@ +namespace Producer.Features.ParseTorrentTitle; + +public class ParsedMovie : BaseParsed +{ +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/ParsedTv.cs b/src/producer/Features/ParseTorrentTitle/ParsedTv.cs new file mode 100644 index 0000000..38fb2ec --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/ParsedTv.cs @@ -0,0 +1,16 @@ +namespace Producer.Features.ParseTorrentTitle; + +public class ParsedTv : BaseParsed +{ + public string? ReleaseTitle { get; set; } + public string? SeriesTitle { get; set; } + public List Seasons { get; set; } = []; + public List EpisodeNumbers { get; set; } = []; + public DateTime? AirDate { get; set; } + public bool FullSeason { get; set; } + public bool IsPartialSeason { get; set; } + public bool IsMultiSeason { get; set; } + public bool IsSeasonExtra { get; set; } + public bool IsSpecial { get; set; } + public int SeasonPart { get; set; } +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/QualityModel.cs b/src/producer/Features/ParseTorrentTitle/QualityModel.cs new file mode 100644 index 0000000..cf3fd9d --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/QualityModel.cs @@ -0,0 +1,9 @@ +namespace Producer.Features.ParseTorrentTitle; + +public class QualityModel +{ + public List Sources { get; set; } = []; + public QualityModifier? Modifier { get; set; } + public Resolution? Resolution { get; set; } + public Revision Revision { get; set; } = new(); +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/QualityModifier.cs b/src/producer/Features/ParseTorrentTitle/QualityModifier.cs new file mode 100644 index 0000000..6aa3f4b --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/QualityModifier.cs @@ -0,0 +1,10 @@ +namespace Producer.Features.ParseTorrentTitle; + +public sealed class QualityModifier : SmartEnum +{ + public static readonly QualityModifier REMUX = new("REMUX", "REMUX"); + public static readonly QualityModifier BRDISK = new("BRDISK", "BRDISK"); + public static readonly QualityModifier RAWHD = new("RAWHD", "RAWHD"); + + private QualityModifier(string name, string value) : base(name, value) { } +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/QualityParser.cs b/src/producer/Features/ParseTorrentTitle/QualityParser.cs new file mode 100644 index 0000000..91d8763 --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/QualityParser.cs @@ -0,0 +1,230 @@ +namespace Producer.Features.ParseTorrentTitle; + +public static partial class QualityParser +{ + [GeneratedRegex(@"\b(?proper|repack|rerip)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex ProperRegex(); + + [GeneratedRegex(@"\b(?REAL)\b", RegexOptions.None, "en-GB")] + private static partial Regex RealRegex(); + + [GeneratedRegex(@"(?v\d\b|\[v\d\])", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex VersionExp(); + + [GeneratedRegex(@"\b(?(BD|UHD)?Remux)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex RemuxExp(); + + [GeneratedRegex(@"\b(COMPLETE|ISO|BDISO|BDMux|BD25|BD50|BR.?DISK)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex BdiskExp(); + + [GeneratedRegex(@"\b(?RawHD|1080i[-_. ]HDTV|Raw[-_. ]HD|MPEG[-_. ]?2)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex RawHdExp(); + + [GeneratedRegex(@"hr[-_. ]ws", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex HighDefPdtvRegex(); + + public static void Parse(string title, out QualityModel result) + { + var normalizedTitle = title.Trim().Replace("_", " ").Replace("[", " ").Replace("]", " ").Trim().ToLower(); + + ParseQualityModifyers(title, out var revision); + ResolutionParser.Parse(normalizedTitle, out var resolution, out _); + SourceParser.ParseSourceGroups(normalizedTitle, out var sourceGroups); + SourceParser.Parse(normalizedTitle, out var source); + VideoCodecsParser.Parse(normalizedTitle, out var codec, out _); + + result = new() + { + Sources = source, + Resolution = resolution, + Revision = revision, + Modifier = null, + }; + + if (BdiskExp().IsMatch(normalizedTitle) && sourceGroups["bluray"]) + { + result.Modifier = QualityModifier.BRDISK; + result.Sources = [Source.BLURAY]; + } + + if (RemuxExp().IsMatch(normalizedTitle) && !sourceGroups["webdl"] && !sourceGroups["hdtv"]) + { + result.Modifier = QualityModifier.REMUX; + result.Sources = [Source.BLURAY]; + } + + if (RawHdExp().IsMatch(normalizedTitle) && result.Modifier != QualityModifier.BRDISK && result.Modifier != QualityModifier.REMUX) + { + result.Modifier = QualityModifier.RAWHD; + result.Sources = [Source.TV]; + } + + if (sourceGroups["bluray"]) + { + result.Sources = [Source.BLURAY]; + + if (codec == VideoCodec.XVID) + { + result.Resolution = Resolution.R480P; + result.Sources = [Source.DVD]; + } + + if (resolution == null) + { + // assume bluray is at least 720p + result.Resolution = Resolution.R720P; + } + + if (resolution == null && result.Modifier == QualityModifier.BRDISK) + { + result.Resolution = Resolution.R1080P; + } + + if (resolution == null && result.Modifier == QualityModifier.REMUX) + { + result.Resolution = Resolution.R2160P; + } + + return; + } + + if (sourceGroups["webdl"] || sourceGroups["webrip"]) + { + result.Sources = source; + + if (resolution == null) + { + result.Resolution = Resolution.R480P; + } + + if (resolution == null) + { + result.Resolution = Resolution.R480P; + } + + if (resolution == null && title.Contains("[WEBDL]")) + { + result.Resolution = Resolution.R720P; + } + + return; + } + + if (sourceGroups["hdtv"]) + { + result.Sources = [Source.TV]; + + if (resolution == null) + { + result.Resolution = Resolution.R480P; + } + + if (resolution == null && title.Contains("[HDTV]")) + { + result.Resolution = Resolution.R720P; + } + + return; + } + + if (sourceGroups["pdtv"] || sourceGroups["sdtv"] || sourceGroups["dsr"] || sourceGroups["tvrip"]) + { + result.Sources = [Source.TV]; + + if (HighDefPdtvRegex().IsMatch(normalizedTitle)) + { + result.Resolution = Resolution.R720P; + return; + } + + result.Resolution = Resolution.R480P; + return; + } + + if (sourceGroups["bdrip"] || sourceGroups["brrip"]) + { + if (codec == VideoCodec.XVID) + { + result.Resolution = Resolution.R480P; + result.Sources = [Source.DVD]; + return; + } + + if (resolution == null) + { + // bdrips are at least 480p + result.Resolution = Resolution.R480P; + } + + result.Sources = [Source.BLURAY]; + return; + } + + if (sourceGroups["workprint"]) + { + result.Sources = [Source.WORKPRINT]; + return; + } + + if (sourceGroups["cam"]) + { + result.Sources = [Source.CAM]; + return; + } + + if (sourceGroups["ts"]) + { + result.Sources = [Source.TELESYNC]; + return; + } + + if (sourceGroups["tc"]) + { + result.Sources = [Source.TELECINE]; + return; + } + + if (result.Modifier == null && (resolution == Resolution.R2160P || resolution == Resolution.R1080P || resolution == Resolution.R720P)) + { + result.Sources = [Source.WEBDL]; + } + } + + private static void ParseQualityModifyers(string title, out Revision revision) + { + var normalizedTitle = title.Trim().Replace("_", " ").Trim().ToLower(); + + revision = new() + { + Version = 1, + Real = 0, + }; + + if (ProperRegex().IsMatch(normalizedTitle)) + { + revision.Version = 2; + } + + var versionResult = VersionExp().Match(normalizedTitle); + if (versionResult.Success) + { + // get numbers from version regex + var digits = Regex.Match(versionResult.Groups["version"].Value, @"\d"); + if (digits.Success) + { + var value = int.Parse(digits.Value); + revision.Version = value; + } + } + + var realCount = 0; + var realGlobalExp = new Regex(RealRegex().ToString(), RegexOptions.None); + // use non normalized title to prevent insensitive REAL matching + while (realGlobalExp.IsMatch(title)) + { + realCount += 1; + } + + revision.Real = realCount; + } +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/Resolution.cs b/src/producer/Features/ParseTorrentTitle/Resolution.cs new file mode 100644 index 0000000..41fc2d8 --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/Resolution.cs @@ -0,0 +1,13 @@ +namespace Producer.Features.ParseTorrentTitle; + +public sealed class Resolution : SmartEnum +{ + public static readonly Resolution R2160P = new("R2160P", "2160P"); + public static readonly Resolution R1080P = new("R1080P", "1080P"); + public static readonly Resolution R720P = new("R720P", "720P"); + public static readonly Resolution R576P = new("R576P", "576P"); + public static readonly Resolution R540P = new("R540P", "540P"); + public static readonly Resolution R480P = new("R480P", "480P"); + + private Resolution(string name, string value) : base(name, value) { } +} diff --git a/src/producer/Features/ParseTorrentTitle/ResolutionParser.cs b/src/producer/Features/ParseTorrentTitle/ResolutionParser.cs new file mode 100644 index 0000000..7311914 --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/ResolutionParser.cs @@ -0,0 +1,53 @@ +namespace Producer.Features.ParseTorrentTitle; + +public partial class ResolutionParser +{ + [GeneratedRegex(@"(?2160p|4k[-_. ](?:UHD|HEVC|BD)|(?:UHD|HEVC|BD)[-_. ]4k|\b(4k)\b|COMPLETE.UHD|UHD.COMPLETE)", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex R2160pExp(); + + [GeneratedRegex(@"(?1080(i|p)|1920x1080)(10bit)?", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex R1080pExp(); + + [GeneratedRegex(@"(?720(i|p)|1280x720|960p)(10bit)?", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex R720pExp(); + + [GeneratedRegex(@"(?576(i|p))", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex R576pExp(); + + [GeneratedRegex(@"(?540(i|p))", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex R540pExp(); + + [GeneratedRegex(@"(?480(i|p)|640x480|848x480)", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex R480Exp(); + + private static readonly Regex ResolutionExp = new(string.Join("|", R2160pExp(), R1080pExp(), R720pExp(), R576pExp(), R540pExp(), R480Exp()), RegexOptions.IgnoreCase); + + public static void Parse(string title, out Resolution? resolution, out string? source) + { + resolution = null; + source = null; + + var result = ResolutionExp.Match(title); + + if (result.Success) + { + foreach (var key in Enum.GetNames(typeof(Resolution))) + { + if (result.Groups[key].Success) + { + resolution = Resolution.FromName(key); + source = result.Groups[key].Value; + return; + } + } + } + + // Fallback to guessing from some sources + // Make safe assumptions like dvdrip is probably 480p + SourceParser.Parse(title, out var sourceList); + if (sourceList.Contains(Source.DVD)) + { + resolution = Resolution.R480P; + } + } +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/Revision.cs b/src/producer/Features/ParseTorrentTitle/Revision.cs new file mode 100644 index 0000000..b5fe42e --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/Revision.cs @@ -0,0 +1,7 @@ +namespace Producer.Features.ParseTorrentTitle; + +public class Revision +{ + public int Version { get; set; } + public int Real { get; set; } +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/Season.cs b/src/producer/Features/ParseTorrentTitle/Season.cs new file mode 100644 index 0000000..96ea2a3 --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/Season.cs @@ -0,0 +1,16 @@ +namespace Producer.Features.ParseTorrentTitle; + +public class Season +{ + public string? ReleaseTitle { get; set; } + public string? SeriesTitle { get; set; } + public List Seasons { get; set; } = []; + public List EpisodeNumbers { get; set; } = []; + public DateTime? AirDate { get; set; } + public bool FullSeason { get; set; } + public bool IsPartialSeason { get; set; } + public bool IsMultiSeason { get; set; } + public bool IsSeasonExtra { get; set; } + public bool IsSpecial { get; set; } + public int SeasonPart { get; set; } +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/SeasonParser.RejectRegex.cs b/src/producer/Features/ParseTorrentTitle/SeasonParser.RejectRegex.cs new file mode 100644 index 0000000..93a3256 --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/SeasonParser.RejectRegex.cs @@ -0,0 +1,44 @@ +namespace Producer.Features.ParseTorrentTitle; + +public static partial class SeasonParser +{ + [GeneratedRegex(@"^[0-9a-zA-Z]{32}", RegexOptions.IgnoreCase)] + private static partial Regex GenericMatchForMd5AndMixedCaseHashesExp(); + + [GeneratedRegex(@"^[a-z0-9]{24}$", RegexOptions.IgnoreCase)] + private static partial Regex GenericMatchForShorterLowerCaseHashesExp(); + + [GeneratedRegex(@"^[A-Z]{11}\d{3}$", RegexOptions.IgnoreCase)] + private static partial Regex FormatSeenOnSomeNZBGeekReleasesExp(); + + [GeneratedRegex(@"^[a-z]{12}\d{3}$", RegexOptions.IgnoreCase)] + private static partial Regex FormatSeenOnSomeNZBGeekReleasesExp2(); + + [GeneratedRegex(@"^Backup_\d{5,}S\d{2}-\d{2}$", RegexOptions.IgnoreCase)] + private static partial Regex BackupFilenameExp(); + + [GeneratedRegex(@"^123$", RegexOptions.IgnoreCase)] + private static partial Regex StartedAppearingDecember2014Exp(); + + [GeneratedRegex(@"^abc$", RegexOptions.IgnoreCase)] + private static partial Regex StartedAppearingJanuary2015Exp(); + + [GeneratedRegex(@"^b00bs$", RegexOptions.IgnoreCase)] + private static partial Regex StartedAppearingJanuary2015Exp2(); + + [GeneratedRegex(@"^\d{6}_\d{2}$", RegexOptions.IgnoreCase)] + private static partial Regex StartedAppearingAugust2018Exp(); + + private static List> _rejectedRegex = + [ + GenericMatchForMd5AndMixedCaseHashesExp, + GenericMatchForShorterLowerCaseHashesExp, + FormatSeenOnSomeNZBGeekReleasesExp, + FormatSeenOnSomeNZBGeekReleasesExp2, + BackupFilenameExp, + StartedAppearingDecember2014Exp, + StartedAppearingJanuary2015Exp, + StartedAppearingJanuary2015Exp2, + StartedAppearingAugust2018Exp + ]; +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/SeasonParser.ValidRegex.cs b/src/producer/Features/ParseTorrentTitle/SeasonParser.ValidRegex.cs new file mode 100644 index 0000000..c7ab0f3 --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/SeasonParser.ValidRegex.cs @@ -0,0 +1,248 @@ +namespace Producer.Features.ParseTorrentTitle; + +public static partial class SeasonParser +{ + [GeneratedRegex(@"^(?19[6-9]\d|20\d\d)(?[-_]?)(?0\d|1[0-2])\k(?[0-2]\d|3[01])(?!\d)", RegexOptions.IgnoreCase)] + private static partial Regex DailyEpisodesWithoutTitleExp(); + + [GeneratedRegex(@"^(?:\W*S?(?(?\d{1,3}(?!\d+)))+){2,}", RegexOptions.IgnoreCase)] + private static partial Regex MultiPartEpisodesWithoutTitleExp(); + + [GeneratedRegex(@"^(?.+?)[-_. ]S(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:[E-_. ]?[ex]?(?<episode>(?<!\d+)\d{1,2}(?!\d+)))+(?:[-_. ]?[ex]?(?<episode1>(?<!\d+)\d{1,2}(?!\d+)))+", RegexOptions.IgnoreCase)] + private static partial Regex MultiEpisodeWithSingleEpisodeNumbersExp(); + + [GeneratedRegex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()[!]))+S?(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))(?:[ex]|\W[ex]|_){1,2}(?<episode>\d{2,3}(?!\d+))(?:(?:-|[ex]|\W[ex]|_){1,2}(?<episode1>\d{2,3}(?!\d+)))+).+?(?:\[.+?\])(?!\\)", RegexOptions.IgnoreCase)] + private static partial Regex MultiEpisodeWithTitleAndTrailingInfoInSlashesExp(); + + [GeneratedRegex(@"(?:S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:[-_]|[ex]){1,2}(?<episode>\d{2,3}(?!\d+))){2,})", RegexOptions.IgnoreCase)] + private static partial Regex EpisodesWithoutTitleMultiExp(); + + [GeneratedRegex(@"^(?:S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:[-_ ]?[ex])(?<episode>\d{2,3}(?!\d+))))", RegexOptions.IgnoreCase)] + private static partial Regex EpisodesWithoutTitleSingleExp(); + + [GeneratedRegex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)(?<title>.+?)[-_. ](?:Episode)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?", RegexOptions.IgnoreCase)] + private static partial Regex AnimeSubGroupTitleEpisodeAbsoluteEpisodeNumberExp(); + + [GeneratedRegex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.)?)(?<title>.+?)(?:(?:[-_\W](?<![()[!]))+(?<absoluteepisode>\d{2,3}(\.\d{1,2})?))+(?:_|-|\s|\.)+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+).*?(?<hash>[([]\w{8}[)\]])?(?:$|\.)", RegexOptions.IgnoreCase)] + private static partial Regex AnimeSubGroupTitleAbsoluteEpisodeNumberSeasonEpisodeExp(); + + [GeneratedRegex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.)?)(?<title>.+?)(?:[-_\W](?<![()[!]))+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)(?:(?:_|-|\s|\.)+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+.*?(?<hash>\[\w{8}\])?(?:$|\.)", RegexOptions.IgnoreCase)] + private static partial Regex AnimeSubGroupTitleSeasonEpisodeAbsoluteEpisodeNumberExp(); + + [GeneratedRegex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.)?)(?<title>.+?)(?:[-_\W](?<![()[!]))+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)(?:\s|\.).*?(?<hash>\[\w{8}\])?(?:$|\.)", RegexOptions.IgnoreCase)] + private static partial Regex AnimeSubGroupTitleSeasonEpisodeExp(); + + [GeneratedRegex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^-]+?\d+?)[-_. ]+(?:[-_. ]?(?<absoluteepisode>\d{3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)", RegexOptions.IgnoreCase)] + private static partial Regex AnimeSubGroupTitleWithTrailingNumberAbsoluteEpisodeNumberExp(); + + [GeneratedRegex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)(?:[. ]-[. ](?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|[-])))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)", RegexOptions.IgnoreCase)] + private static partial Regex AnimeSubGroupTitleAbsoluteEpisodeNumberExp(); + + [GeneratedRegex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)[-_. ]+\(?(?:[-_. ]?#?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+\)?(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)", RegexOptions.IgnoreCase)] + private static partial Regex AnimeSubGroupTitleAbsoluteEpisodeNumberSpecialExp(); + + [GeneratedRegex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()[!]))+S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:[ex]|[-_. ]e){1,2}(?<episode>\d{1,3}(?!\d+)))+){2,}", RegexOptions.IgnoreCase)] + private static partial Regex MultiEpisodeRepeatedExp(); + + [GeneratedRegex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()[!]))+S?(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))(?:[ex]|\W[ex]){1,2}(?<episode>(?!265|264)\d{2,3}(?!\d+|(?:[ex]|\W[ex]|_|-){1,2})))", RegexOptions.IgnoreCase)] + private static partial Regex SingleEpisodesWithTitleExp(); + + [GeneratedRegex(@"^(?<title>.+?)(?:[-_\W](?<![()[!]))+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]|\W[ex]){1,2}(?<episode>(?<!\d+)\d{2}(?!\d+)))).+?(?:[-_. ]?(?<absoluteepisode>(?<!\d+)\d{3}(\.\d{1,2})?(?!\d+)))+.+?\[(?<subgroup>.+?)\](?:$|\.mkv)", RegexOptions.IgnoreCase)] + private static partial Regex AnimeTitleSeasonEpisodeNumberAbsoluteEpisodeNumberSubGroupExp(); + + [GeneratedRegex(@"^(?<title>.+?)[-_. ]Episode(?:[-_. ]+(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:.+?)\[(?<subgroup>.+?)\].*?(?<hash>\[\w{8}\])?(?:$|\.)", RegexOptions.IgnoreCase)] + private static partial Regex AnimeTitleEpisodeAbsoluteEpisodeNumberSubGroupHashExp(); + + [GeneratedRegex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{3}(\.\d{1,2})(?!\d+)))+(?:.+?)\[(?<subgroup>.+?)\].*?(?<hash>\[\w{8}\])?(?:$|\.)", RegexOptions.IgnoreCase)] + private static partial Regex AnimeTitleAbsoluteEpisodeNumberSubGroupHashExp(); + + [GeneratedRegex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?[-_. ]+.*?(?<hash>\[\w{8}\])(?:$|\.)", RegexOptions.IgnoreCase)] + private static partial Regex AnimeTitleAbsoluteEpisodeNumberHashExp(); + + [GeneratedRegex(@"^(?<title>.+?)?\W*(?<airdate>\d{4}\W+[0-1][0-9]\W+[0-3][0-9])(?!\W+[0-3][0-9])[-_. ](?:s?(?<season>(?<!\d+)(?:\d{1,2})(?!\d+)))(?:[ex](?<episode>(?<!\d+)(?:\d{1,3})(?!\d+)))/i", RegexOptions.IgnoreCase)] + private static partial Regex EpisodesWithAirdateAndSeasonEpisodeNumberCaptureSeasonEpisodeOnlyExp(); + + [GeneratedRegex(@"^(?<title>.+?)?\W*(?<airyear>\d{4})\W+(?<airmonth>[0-1][0-9])\W+(?<airday>[0-3][0-9])(?!\W+[0-3][0-9]).+?(?:s?(?<season>(?<!\d+)(?:\d{1,2})(?!\d+)))(?:[ex](?<episode>(?<!\d+)(?:\d{1,3})(?!\d+)))/i", RegexOptions.IgnoreCase)] + private static partial Regex EpisodesWithAirdateAndSeasonEpisodeNumberExp(); + + [GeneratedRegex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()[!]))+S(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))(?:e|\We|_){1,2}(?<episode>\d{2,3}(?!\d+))(?:(?:-|e|\We|_){1,2}(?<episode1>\d{2,3}(?!\d+)))*)\W?(?!\\)", RegexOptions.IgnoreCase)] + private static partial Regex EpisodesWithTitleSingleEpisodesMultiEpisodeExp(); + + [GeneratedRegex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()[!]))+S(?<season>(?<!\d+)(?:\d{4})(?!\d+))(?:e|\We|_){1,2}(?<episode>\d{2,3}(?!\d+))(?:(?:-|e|\We|_){1,2}(?<episode1>\d{2,3}(?!\d+)))*)\W?(?!\\)", RegexOptions.IgnoreCase)] + private static partial Regex EpisodesWithTitle4DigitSeasonNumberSingleEpisodesMultiEpisodeExp(); + + [GeneratedRegex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()[!]))+(?<season>(?<!\d+)(?:\d{4})(?!\d+))(?:x|\Wx){1,2}(?<episode>\d{2,3}(?!\d+))(?:(?:-|x|\Wx|_){1,2}(?<episode1>\d{2,3}(?!\d+)))*)\W?(?!\\)", RegexOptions.IgnoreCase)] + private static partial Regex EpisodesWithTitle4DigitSeasonNumberSingleEpisodesMultiEpisodeExp2(); + + [GeneratedRegex(@"^(?<title>.+?)[-_. ]+S(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))\W?-\W?S?(?<season1>(?<!\d+)(?:\d{1,2})(?!\d+))", RegexOptions.IgnoreCase)] + private static partial Regex MultiSeasonPackExp(); + + [GeneratedRegex(@"^(?<title>.+?)(?:\W+S(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))\W+(?:(?:Part\W?|(?<!\d+\W+)e)(?<seasonpart>\d{1,2}(?!\d+)))+)", RegexOptions.IgnoreCase)] + private static partial Regex PartialSeasonPackExp(); + + [GeneratedRegex(@"^(?<title>.+?\d{4})(?:\W+(?:(?:Part\W?|e)(?<episode>\d{1,2}(?!\d+)))+)", RegexOptions.IgnoreCase)] + private static partial Regex MiniSeriesWithYearInTitleExp(); + + [GeneratedRegex(@"^(?<title>.+?)(?:[-._ ][e])(?<episode>\d{2,3}(?!\d+))(?:(?:-?[e])(?<episode1>\d{2,3}(?!\d+)))+", RegexOptions.IgnoreCase)] + private static partial Regex MiniSeriesMultiEpisodesExp(); + + [GeneratedRegex(@"^(?<title>.+?)(?:\W+(?:(?:Part\W?|(?<!\d+\W+)e)(?<episode>\d{1,2}(?!\d+)))+)", RegexOptions.IgnoreCase)] + private static partial Regex MiniSeriesEpisodesExp(); + + [GeneratedRegex(@"^(?<title>.+?)(?:\W+(?:Part[-._ ](?<episode>One|Two|Three|Four|Five|Six|Seven|Eight|Nine)(>[-._ ])))", RegexOptions.IgnoreCase)] + private static partial Regex MiniSeriesEpisodesExp2(); + + [GeneratedRegex(@"^(?<title>.+?)(?:\W+(?:(?<episode>(?<!\d+)\d{1,2}(?!\d+))of\d+)+)", RegexOptions.IgnoreCase)] + private static partial Regex MiniSeriesEpisodesExp3(); + + [GeneratedRegex(@"(?:.*(?:""|^))(?<title>.*?)(?:[-_\W](?<![()[]))+(?:\W?Season\W?)(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:\W|_)+(?:Episode\W)(?:[-_. ]?(?<episode>(?<!\d+)\d{1,2}(?!\d+)))+", RegexOptions.IgnoreCase)] + private static partial Regex SupportsSeason01Episode03Exp(); + + [GeneratedRegex(@"(?:.*(?:^))(?<title>.*?)[-._ ]+\[S(?<season>(?<!\d+)\d{2}(?!\d+))(?:[E-]{1,2}(?<episode>(?<!\d+)\d{2}(?!\d+)))+\]", RegexOptions.IgnoreCase)] + private static partial Regex MultiEpisodeWithEpisodesInSquareBracketsExp(); + + [GeneratedRegex(@"(?:.*(?:^))(?<title>.*?)S(?<season>(?<!\d+)\d{2}(?!\d+))(?:E(?<episode>(?<!\d+)\d{2}(?!\d+)))+", RegexOptions.IgnoreCase)] + private static partial Regex MultiEpisodeReleaseWithNoSpaceBetweenSeriesTitleAndSeasonExp(); + + [GeneratedRegex(@"(?:.*(?:""|^))(?<title>.*?)(?:\W?|_)S(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:\W|_)?Ep?[ ._]?(?<episode>(?<!\d+)\d{1,2}(?!\d+))", RegexOptions.IgnoreCase)] + private static partial Regex SingleEpisodeSeasonOrEpisodeExp(); + + [GeneratedRegex(@"(?:.*(?:""|^))(?<title>.*?)(?:\W?|_)S(?<season>(?<!\d+)\d{3}(?!\d+))(?:\W|_)?E(?<episode>(?<!\d+)\d{1,2}(?!\d+))", RegexOptions.IgnoreCase)] + private static partial Regex ThreeDigitSeasonExp(); + + [GeneratedRegex(@"^(?:(?<title>.+?)(?:_|-|\s|\.)+)(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+)))(?:(?:-|[ex]|\W[ex]|_){1,2}(?<episode>(?<!\d+)\d{5}(?!\d+)))", RegexOptions.IgnoreCase)] + private static partial Regex FiveDigitEpisodeNumberWithTitleExp(); + + [GeneratedRegex(@"^(?:(?<title>.+?)(?:_|-|\s|\.)+)(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+)))(?:(?:[-_. ]{1,3}ep){1,2}(?<episode>(?<!\d+)\d{5}(?!\d+)))", RegexOptions.IgnoreCase)] + private static partial Regex FiveDigitMultiEpisodeWithTitleExp(); + + [GeneratedRegex(@"^(?<title>.+?)(?:_|-|\s|\.)+S(?<season>\d{2}(?!\d+))(\W-\W)E(?<episode>(?<!\d+)\d{2}(?!\d+))(?!\\)", RegexOptions.IgnoreCase)] + private static partial Regex SeparatedSeasonAndEpisodeNumbersExp(); + + [GeneratedRegex(@"^(?<title>.+?S\d{1,2})[-_. ]{3,}(?:EP)?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+))", RegexOptions.IgnoreCase)] + private static partial Regex AnimeTitleWithSeasonNumberAbsoluteEpisodeNumberExp(); + + [GeneratedRegex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)[-_. ]+?(?:Episode[-_. ]+?)(?<absoluteepisode>\d{1}(\.\d{1,2})?(?!\d+))", RegexOptions.IgnoreCase)] + private static partial Regex AnimeFrenchTitlesWithSingleEpisodeNumbersExp(); + + [GeneratedRegex(@"^(?<title>.+?)\W(?:S|Season)\W?(?<season>\d{1,2}(?!\d+))(\W+|_|$)(?<extras>EXTRAS|SUBPACK)?(?!\\)", RegexOptions.IgnoreCase)] + private static partial Regex SeasonOnlyReleasesExp(); + + [GeneratedRegex(@"^(?<title>.+?)\W(?:S|Season)\W?(?<season>\d{4}(?!\d+))(\W+|_|$)(?<extras>EXTRAS|SUBPACK)?(?!\\)", RegexOptions.IgnoreCase)] + private static partial Regex FourDigitSeasonOnlyReleasesExp(); + + [GeneratedRegex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()[!]))+\[S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:-|[ex]|\W[ex]|_){1,2}(?<episode>(?<!\d+)\d{2}(?!\d+|i|p)))+\])\W?(?!\\)", RegexOptions.IgnoreCase)] + private static partial Regex EpisodesWithTitleAndSeasonEpisodeInSquareBracketsExp(); + + [GeneratedRegex(@"^(?<title>.+?)?(?:(?:[_.](?<![()[!]))+(?<season>(?<!\d+)[1-9])(?<episode>[1-9][0-9]|[0][1-9])(?![a-z]|\d+))+(?:[_.]|$)", RegexOptions.IgnoreCase)] + private static partial Regex Supports103_113NamingExp(); + + [GeneratedRegex(@"^(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:-|[ex]|\W[ex]|_){1,2}(?<episode>\d{4}(?!\d+|i|p)))+)(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase)] + private static partial Regex FourDigitEpisodeNumberEpisodesWithoutTitleSingleAndMultiExp(); + + [GeneratedRegex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()[!]))+S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:-|[ex]|\W[ex]|_){1,2}(?<episode>\d{4}(?!\d+|i|p)))+)\W?(?!\\)", RegexOptions.IgnoreCase)] + private static partial Regex FourDigitEpisodeNumberEpisodesWithTitleSingleAndMultiExp(); + + [GeneratedRegex(@"^(?<title>.+?)?\W*(?<airyear>\d{4})[-_. ]+(?<airmonth>[0-1][0-9])[-_. ]+(?<airday>[0-3][0-9])(?![-_. ]+[0-3][0-9])", RegexOptions.IgnoreCase)] + private static partial Regex EpisodesWithAirdateExp(); + + [GeneratedRegex(@"^(?<title>.+?)?\W*(?<airmonth>[0-1][0-9])[-_. ]+(?<airday>[0-3][0-9])[-_. ]+(?<airyear>\d{4})(?!\d+)", RegexOptions.IgnoreCase)] + private static partial Regex EpisodesWithAirdateExp2(); + + [GeneratedRegex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![()[!]))*(?<season>(?<!\d+|\(|\[|e|x)\d{2})(?<episode>(?<!e|x)\d{2}(?!p|i|\d+|\)|\]|\W\d+|\W(?:e|ep|x)\d+)))+(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase)] + private static partial Regex Supports1103_1113NamingExp(); + + [GeneratedRegex(@"^(?<title>.*?)(?:(?:[-_\W](?<![()[!]))+S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:-|[ex]){1,2}(?<episode>\d{1}))+)+(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase)] + private static partial Regex EpisodesWithSingleDigitEpisodeNumberExp(); + + [GeneratedRegex(@"^(?:Season(?:_|-|\s|\.)(?<season>(?<!\d+)\d{1,2}(?!\d+)))(?:_|-|\s|\.)(?<episode>(?<!\d+)\d{1,2})", RegexOptions.IgnoreCase)] + private static partial Regex ITunesSeason1_05TitleQualityExp(); + + [GeneratedRegex(@"^(?:(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))(?:-(?<episode>\d{2,3}(?!\d+))))", RegexOptions.IgnoreCase)] + private static partial Regex ITunes1_05TitleQualityExp(); + + [GeneratedRegex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:_|\s|\.)+(?:e|ep)(?<absoluteepisode>\d{2,3}(\.\d{1,2})?)-(?<absoluteepisode1>(?<!\d+)\d{1,2}(\.\d{1,2})?(?!\d+|-)).*?(?<hash>\[\w{8}\])?(?:$|\.)", RegexOptions.IgnoreCase)] + private static partial Regex AnimeRange_TitleAbsoluteEpisodeNumberExp(); + + [GeneratedRegex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:_|-|\s|\.)+(?:e|ep)(?<absoluteepisode>\d{2,4}(\.\d{1,2})?))+.*?(?<hash>\[\w{8}\])?(?:$|\.)", RegexOptions.IgnoreCase)] + private static partial Regex Anime_TitleAbsoluteEpisodeNumberExp(); + + [GeneratedRegex(@"^(?<title>.+?)[-_. ](?:Episode)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?", RegexOptions.IgnoreCase)] + private static partial Regex Anime_TitleEpisodeAbsoluteEpisodeNumberExp(); + + [GeneratedRegex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)[_. ]+(?<absoluteepisode>(?<!\d+)\d{1,2}(\.\d{1,2})?(?!\d+))-(?<absoluteepisode1>(?<!\d+)\d{1,2}(\.\d{1,2})?(?!\d+|-))(?:_|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?", RegexOptions.IgnoreCase)] + private static partial Regex AnimeRange_TitleAbsoluteEpisodeNumberExp2(); + + [GeneratedRegex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?", RegexOptions.IgnoreCase)] + private static partial Regex Anime_TitleAbsoluteEpisodeNumberExp2(); + + [GeneratedRegex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:[-_\W](?<![()[!]))+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?", RegexOptions.IgnoreCase)] + private static partial Regex Anime_TitleAbsoluteEpisodeNumberExp3(); + + [GeneratedRegex(@"^(?<title>.+?)[-_. ](?<season>[0]?\d?)(?:(?<episode>\d{2}){2}(?!\d+))[-_. ]", RegexOptions.IgnoreCase)] + private static partial Regex ExtantTerribleMultiEpisodeNamingExp(); + + + private static List<Func<Regex>> _validRegexes = + [ + DailyEpisodesWithoutTitleExp, + MultiPartEpisodesWithoutTitleExp, + MultiEpisodeWithSingleEpisodeNumbersExp, + MultiEpisodeWithTitleAndTrailingInfoInSlashesExp, + EpisodesWithoutTitleMultiExp, + EpisodesWithoutTitleSingleExp, + AnimeSubGroupTitleEpisodeAbsoluteEpisodeNumberExp, + AnimeSubGroupTitleAbsoluteEpisodeNumberSeasonEpisodeExp, + AnimeSubGroupTitleSeasonEpisodeAbsoluteEpisodeNumberExp, + AnimeSubGroupTitleSeasonEpisodeExp, + AnimeSubGroupTitleWithTrailingNumberAbsoluteEpisodeNumberExp, + AnimeSubGroupTitleAbsoluteEpisodeNumberExp, + AnimeSubGroupTitleAbsoluteEpisodeNumberSpecialExp, + MultiEpisodeRepeatedExp, + SingleEpisodesWithTitleExp, + AnimeTitleSeasonEpisodeNumberAbsoluteEpisodeNumberSubGroupExp, + AnimeTitleEpisodeAbsoluteEpisodeNumberSubGroupHashExp, + AnimeTitleAbsoluteEpisodeNumberSubGroupHashExp, + AnimeTitleAbsoluteEpisodeNumberHashExp, + EpisodesWithAirdateAndSeasonEpisodeNumberCaptureSeasonEpisodeOnlyExp, + EpisodesWithAirdateAndSeasonEpisodeNumberExp, + EpisodesWithTitleSingleEpisodesMultiEpisodeExp, + EpisodesWithTitle4DigitSeasonNumberSingleEpisodesMultiEpisodeExp, + EpisodesWithTitle4DigitSeasonNumberSingleEpisodesMultiEpisodeExp2, + MultiSeasonPackExp, + PartialSeasonPackExp, + MiniSeriesWithYearInTitleExp, + MiniSeriesMultiEpisodesExp, + MiniSeriesEpisodesExp, + MiniSeriesEpisodesExp2, + MiniSeriesEpisodesExp3, + SupportsSeason01Episode03Exp, + MultiEpisodeWithEpisodesInSquareBracketsExp, + MultiEpisodeReleaseWithNoSpaceBetweenSeriesTitleAndSeasonExp, + SingleEpisodeSeasonOrEpisodeExp, + ThreeDigitSeasonExp, + FiveDigitEpisodeNumberWithTitleExp, + SeparatedSeasonAndEpisodeNumbersExp, + AnimeTitleWithSeasonNumberAbsoluteEpisodeNumberExp, + AnimeFrenchTitlesWithSingleEpisodeNumbersExp, + SeasonOnlyReleasesExp, + FourDigitSeasonOnlyReleasesExp, + EpisodesWithTitleAndSeasonEpisodeInSquareBracketsExp, + Supports103_113NamingExp, + FourDigitEpisodeNumberEpisodesWithoutTitleSingleAndMultiExp, + FourDigitEpisodeNumberEpisodesWithTitleSingleAndMultiExp, + EpisodesWithAirdateExp, + EpisodesWithAirdateExp2, + Supports1103_1113NamingExp, + EpisodesWithSingleDigitEpisodeNumberExp, + ITunesSeason1_05TitleQualityExp, + ITunes1_05TitleQualityExp, + AnimeRange_TitleAbsoluteEpisodeNumberExp, + Anime_TitleAbsoluteEpisodeNumberExp, + Anime_TitleEpisodeAbsoluteEpisodeNumberExp, + AnimeRange_TitleAbsoluteEpisodeNumberExp2, + Anime_TitleAbsoluteEpisodeNumberExp2, + Anime_TitleAbsoluteEpisodeNumberExp3, + ExtantTerribleMultiEpisodeNamingExp, + ]; +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/SeasonParser.cs b/src/producer/Features/ParseTorrentTitle/SeasonParser.cs new file mode 100644 index 0000000..4ce82f2 --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/SeasonParser.cs @@ -0,0 +1,303 @@ +namespace Producer.Features.ParseTorrentTitle; + +public static partial class SeasonParser +{ + [GeneratedRegex(@"^(?:\[.+?\])+", RegexOptions.None)] + private static partial Regex RequestInfoExp(); + + [GeneratedRegex(@"(?<=[_.-])(?<airdate>(?<!\d)(?<airyear>[1-9]\d{1})(?<airmonth>[0-1][0-9])(?<airday>[0-3][0-9]))(?=[_.-])", RegexOptions.IgnoreCase)] + private static partial Regex SixDigitAirDateMatchExp(); + + public static Season Parse(string title) + { + if (!PreValidation(title)) + { + return null; + } + + var simpleTitle = TitleParser.SimplifyTitle(title); + + // parse daily episodes with mmddyy eg `At.Midnight.140722.720p.HDTV.x264-YesTV` + var sixDigitAirDateMatch = SixDigitAirDateMatchExp().Match(title); + + if (sixDigitAirDateMatch.Groups.Count > 0) + { + var airYear = sixDigitAirDateMatch.Groups["airyear"]?.Value ?? ""; + var airMonth = sixDigitAirDateMatch.Groups["airmonth"]?.Value ?? ""; + var airDay = sixDigitAirDateMatch.Groups["airday"]?.Value ?? ""; + + if (airMonth != "00" || airDay != "00") + { + var fixedDate = $"20{airYear}.{airMonth}.{airDay}"; + + simpleTitle = simpleTitle.Replace(sixDigitAirDateMatch.Groups["airdate"]?.Value ?? "", fixedDate); + } + } + + foreach (var exp in _validRegexes) + { + var match = exp().Match(simpleTitle); + + if (match.Groups.Count > 0) + { + var result = ParseMatchCollection(match, simpleTitle); + + if (result.FullSeason && result.ReleaseTokens != null && result.ReleaseTokens.Contains("Special", StringComparison.OrdinalIgnoreCase)) + { + result.FullSeason = false; + result.IsSpecial = true; + } + + return new() + { + ReleaseTitle = title, + SeriesTitle = result.SeriesName, + // SeriesTitleInfo = 0, + Seasons = result.SeasonNumbers ?? [], + EpisodeNumbers = result.EpisodeNumbers ?? [], + AirDate = result.AirDate, + FullSeason = result.FullSeason, + IsPartialSeason = result.IsPartialSeason ?? false, + IsMultiSeason = result.IsMultiSeason ?? false, + IsSeasonExtra = result.IsSeasonExtra ?? false, + IsSpecial = result.IsSpecial ?? false, + SeasonPart = result.SeasonPart ?? 0, + }; + } + } + + return null; + } + + private static ParsedMatch ParseMatchCollection(Match match, string simpleTitle) + { + var groups = match.Groups; + + if (groups.Count == 0) + { + throw new("No match"); + } + + var seriesName = (groups["title"]?.Value ?? "") + .Replace(".", " ") + .Replace("_", " ") + .Replace(RequestInfoExp().ToString(), "") + .Trim(); + + var result = new ParsedMatch + { + SeriesName = seriesName, + }; + + var lastSeasonEpisodeStringIndex = IndexOfEnd(simpleTitle, groups["title"]?.Value ?? ""); + + if (int.TryParse(groups["airyear"]?.Value, out var airYear) && airYear >= 1900) + { + var seasons = new List<string> {groups["season"]?.Value, groups["season1"]?.Value} + .Where(x => !string.IsNullOrEmpty(x)) + .Select( + x => + { + lastSeasonEpisodeStringIndex = Math.Max( + IndexOfEnd(simpleTitle, x ?? ""), + lastSeasonEpisodeStringIndex + ); + return int.Parse(x); + }) + .ToList(); + + if (seasons.Count > 1) + { + seasons = CompleteRange(seasons); + } + + result.SeasonNumbers = seasons; + + if (seasons.Count > 1) + { + result.IsMultiSeason = true; + } + + var episodeCaptures = new List<string> {groups["episode"]?.Value, groups["episode1"]?.Value} + .Where(x => !string.IsNullOrEmpty(x)) + .ToList(); + + var absoluteEpisodeCaptures = new List<string> {groups["absoluteepisode"]?.Value, groups["absoluteepisode1"]?.Value} + .Where(x => !string.IsNullOrEmpty(x)) + .ToList(); + + // handle 0 episode possibly indicating a full season release + if (episodeCaptures.Any()) + { + var first = int.Parse(episodeCaptures[0]); + var last = int.Parse(episodeCaptures[^1]); + + if (first > last) + { + return null; + } + + var count = last - first + 1; + result.EpisodeNumbers = Enumerable.Range(first, count).ToList(); + } + + if (absoluteEpisodeCaptures.Any()) + { + var first = double.Parse(absoluteEpisodeCaptures[0]); + var last = double.Parse(absoluteEpisodeCaptures[^1]); + + if (first % 1 != 0 || last % 1 != 0) + { + if (absoluteEpisodeCaptures.Count != 1) + { + return null; + } + + // specialAbsoluteEpisodeNumbers in radarr + result.EpisodeNumbers = new() + {(int) first}; + result.IsSpecial = true; + + lastSeasonEpisodeStringIndex = Math.Max( + IndexOfEnd(simpleTitle, absoluteEpisodeCaptures[0] ?? ""), + lastSeasonEpisodeStringIndex + ); + } + else + { + var count = (int) (last - first + 1); + // AbsoluteEpisodeNumbers in radarr + result.EpisodeNumbers = Enumerable.Range((int) first, count).ToList(); + + if (groups["special"]?.Value != null) + { + result.IsSpecial = true; + } + } + } + + if (!episodeCaptures.Any() && !absoluteEpisodeCaptures.Any()) + { + // Check to see if this is an "Extras" or "SUBPACK" release, if it is, set + // IsSeasonExtra so they can be filtered out + if (groups["extras"]?.Value != null) + { + result.IsSeasonExtra = true; + } + + // Partial season packs will have a seasonpart group so they can be differentiated + // from a full season/single episode release + var seasonPart = groups["seasonpart"]?.Value; + + if (seasonPart != null) + { + result.SeasonPart = int.Parse(seasonPart); + result.IsPartialSeason = true; + } + else + { + result.FullSeason = true; + } + } + + if (absoluteEpisodeCaptures.Any() && result.EpisodeNumbers == null) + { + result.SeasonNumbers = new() + {0}; + } + } + else + { + if (int.TryParse(groups["airmonth"]?.Value, out var airMonth) && int.TryParse(groups["airday"]?.Value, out var airDay)) + { + // Swap day and month if month is bigger than 12 (scene fail) + if (airMonth > 12) + { + (airDay, airMonth) = (airMonth, airDay); + } + + var airDate = new DateTime(airYear, airMonth, airDay); + + // dates in the future is most likely parser error + if (airDate > DateTime.Now) + { + throw new("Parsed date is in the future"); + } + + if (airDate < new DateTime(1970, 1, 1)) + { + throw new("Parsed date error"); + } + + lastSeasonEpisodeStringIndex = Math.Max( + IndexOfEnd(simpleTitle, groups["airyear"]?.Value ?? ""), + lastSeasonEpisodeStringIndex + ); + lastSeasonEpisodeStringIndex = Math.Max( + IndexOfEnd(simpleTitle, groups["airmonth"]?.Value ?? ""), + lastSeasonEpisodeStringIndex + ); + lastSeasonEpisodeStringIndex = Math.Max( + IndexOfEnd(simpleTitle, groups["airday"]?.Value ?? ""), + lastSeasonEpisodeStringIndex + ); + result.AirDate = airDate; + } + } + + if (lastSeasonEpisodeStringIndex == simpleTitle.Length || lastSeasonEpisodeStringIndex == -1) + { + result.ReleaseTokens = simpleTitle; + } + else + { + result.ReleaseTokens = simpleTitle.Substring(lastSeasonEpisodeStringIndex); + } + + result.SeriesTitle = seriesName; + // TODO: seriesTitleInfo + + return result; + } + + private static bool PreValidation(string title) => + _rejectedRegex.Select(exp => exp().Match(title)).All(match => !match.Success); + + private static List<int> CompleteRange(List<int> arr) + { + var uniqArr = arr.Distinct().ToList(); + + var first = uniqArr[0]; + var last = uniqArr[^1]; + + if (first > last) + { + return arr; + } + + var count = last - first + 1; + return Enumerable.Range(first, count).ToList(); + } + + private static int IndexOfEnd(string str1, string str2) + { + var io = str1.IndexOf(str2, StringComparison.Ordinal); + return io == -1 ? -1 : io + str2.Length; + } + + private record ParsedMatch + { + public string? SeriesName { get; set; } + public string? SeriesTitle { get; set; } + public List<int>? SeasonNumbers { get; set; } + public bool? IsMultiSeason { get; set; } + public List<int>? EpisodeNumbers { get; set; } + public bool? IsSpecial { get; set; } + public bool? IsSeasonExtra { get; set; } + public int? SeasonPart { get; set; } + public bool? IsPartialSeason { get; set; } + public bool FullSeason { get; set; } + public DateTime? AirDate { get; set; } + public string? ReleaseTokens { get; set; } + } +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/Source.cs b/src/producer/Features/ParseTorrentTitle/Source.cs new file mode 100644 index 0000000..8836539 --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/Source.cs @@ -0,0 +1,20 @@ +namespace Producer.Features.ParseTorrentTitle; + +public sealed class Source : SmartEnum<Source, string> +{ + public static readonly Source BLURAY = new("BLURAY", "BLURAY"); + public static readonly Source WEBDL = new("WEBDL", "WEBDL"); + public static readonly Source WEBRIP = new("WEBRIP", "WEBRIP"); + public static readonly Source DVD = new("DVD", "DVD"); + public static readonly Source CAM = new("CAM", "CAM"); + public static readonly Source SCREENER = new("SCREENER", "SCREENER"); + public static readonly Source PPV = new("PPV", "PPV"); + public static readonly Source TELESYNC = new("TELESYNC", "TELESYNC"); + public static readonly Source TELECINE = new("TELECINE", "TELECINE"); + public static readonly Source WORKPRINT = new("WORKPRINT", "WORKPRINT"); + public static readonly Source TV = new("TV", "TV"); + + private Source(string name, string value) : base(name, value) + { + } +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/SourceParser.cs b/src/producer/Features/ParseTorrentTitle/SourceParser.cs new file mode 100644 index 0000000..7453852 --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/SourceParser.cs @@ -0,0 +1,151 @@ +namespace Producer.Features.ParseTorrentTitle; + +public static partial class SourceParser +{ + [GeneratedRegex(@"\b(?<bluray>M?Blu-?Ray|HDDVD|BD|UHDBD|BDISO|BDMux|BD25|BD50|BR.?DISK|Bluray(1080|720)p?|BD(1080|720)p?)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex BlurayExp(); + + [GeneratedRegex(@"\b(?<webdl>WEB[-_. ]DL|HDRIP|WEBDL|WEB-DLMux|NF|APTV|NETFLIX|NetflixU?HD|DSNY|DSNP|HMAX|AMZN|AmazonHD|iTunesHD|MaxdomeHD|WebHD|WEB$|[. ]WEB[. ](?:[xh]26[45]|DD5[. ]1)|\d+0p[. ]WEB[. ]|\b\s\/\sWEB\s\/\s\b|AMZN[. ]WEB[. ])\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex WebdlExp(); + + [GeneratedRegex(@"\b(?<webrip>WebRip|Web-Rip|WEBCap|WEBMux)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex WebripExp(); + + [GeneratedRegex(@"\b(?<hdtv>HDTV)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex HdtvExp(); + + [GeneratedRegex(@"\b(?<bdrip>BDRip)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex BdripExp(); + + [GeneratedRegex(@"\b(?<brrip>BRRip)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex BrripExp(); + + [GeneratedRegex(@"\b(?<scr>SCR|SCREENER|DVDSCR|(DVD|WEB).?SCREENER)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex ScrExp(); + + [GeneratedRegex(@"\b(?<dvdr>DVD-R|DVDR)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex DvdrExp(); + + [GeneratedRegex(@"\b(?<dvd>DVD9?|DVDRip|NTSC|PAL|xvidvd|DvDivX)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex DvdExp(); + + [GeneratedRegex(@"\b(?<dsr>WS[-_. ]DSR|DSR)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex DsrExp(); + + [GeneratedRegex(@"\b(?<regional>R[0-9]{1}|REGIONAL)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex RegionalExp(); + + [GeneratedRegex(@"\b(?<ppv>PPV)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex PpvExp(); + + [GeneratedRegex(@"\b(?<ts>TS|TELESYNC|HD-TS|HDTS|PDVD|TSRip|HDTSRip)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex TsExp(); + + [GeneratedRegex(@"\b(?<tc>TC|TELECINE|HD-TC|HDTC)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex TcExp(); + + [GeneratedRegex(@"\b(?<cam>CAMRIP|CAM|HDCAM|HD-CAM)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex CamExp(); + + [GeneratedRegex(@"\b(?<workprint>WORKPRINT|WP)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex WorkprintExp(); + + [GeneratedRegex(@"\b(?<pdtv>PDTV)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex PdtvExp(); + + [GeneratedRegex(@"\b(?<sdtv>SDTV)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex SdtvExp(); + + [GeneratedRegex(@"\b(?<tvrip>TVRip)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex TvripExp(); + + public static void Parse(string title, out List<Source> result) + { + ParseSourceGroups(title, out var groups); + + result = []; + + if (groups["bluray"] || groups["bdrip"] || groups["brrip"]) + { + result.Add(Source.BLURAY); + } + + if (groups["webrip"]) + { + result.Add(Source.WEBRIP); + } + + if (!groups["webrip"] && groups["webdl"]) + { + result.Add(Source.WEBDL); + } + + if (groups["dvdr"] || (groups["dvd"] && !groups["scr"])) + { + result.Add(Source.DVD); + } + + if (groups["ppv"]) + { + result.Add(Source.PPV); + } + + if (groups["workprint"]) + { + result.Add(Source.WORKPRINT); + } + + if (groups["pdtv"] || groups["sdtv"] || groups["dsr"] || groups["tvrip"] || groups["hdtv"]) + { + result.Add(Source.TV); + } + + if (groups["cam"]) + { + result.Add(Source.CAM); + } + + if (groups["ts"]) + { + result.Add(Source.TELESYNC); + } + + if (groups["tc"]) + { + result.Add(Source.TELECINE); + } + + if (groups["scr"]) + { + result.Add(Source.SCREENER); + } + } + + public static void ParseSourceGroups(string title, out Dictionary<string, bool> groups) + { + var normalizedName = title.Replace("_", " ").Replace("[", " ").Replace("]", " ").Trim(); + + groups = new() + { + {"bluray", BlurayExp().IsMatch(normalizedName)}, + {"webdl", WebdlExp().IsMatch(normalizedName)}, + {"webrip", WebripExp().IsMatch(normalizedName)}, + {"hdtv", HdtvExp().IsMatch(normalizedName)}, + {"bdrip", BdripExp().IsMatch(normalizedName)}, + {"brrip", BrripExp().IsMatch(normalizedName)}, + {"scr", ScrExp().IsMatch(normalizedName)}, + {"dvdr", DvdrExp().IsMatch(normalizedName)}, + {"dvd", DvdExp().IsMatch(normalizedName)}, + {"dsr", DsrExp().IsMatch(normalizedName)}, + {"regional", RegionalExp().IsMatch(normalizedName)}, + {"ppv", PpvExp().IsMatch(normalizedName)}, + {"ts", TsExp().IsMatch(normalizedName)}, + {"tc", TcExp().IsMatch(normalizedName)}, + {"cam", CamExp().IsMatch(normalizedName)}, + {"workprint", WorkprintExp().IsMatch(normalizedName)}, + {"pdtv", PdtvExp().IsMatch(normalizedName)}, + {"sdtv", SdtvExp().IsMatch(normalizedName)}, + {"tvrip", TvripExp().IsMatch(normalizedName)}, + }; + } +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/TitleParser.cs b/src/producer/Features/ParseTorrentTitle/TitleParser.cs new file mode 100644 index 0000000..e559ee0 --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/TitleParser.cs @@ -0,0 +1,200 @@ +namespace Producer.Features.ParseTorrentTitle; + +public static partial class TitleParser +{ + [GeneratedRegex(@"^(?<title>(?![([]).+?)?(?:(?:[-_\W](?<![)[!]))*\(?\b(?<edition>(((Extended.|Ultimate.)?(Director.?s|Collector.?s|Theatrical|Anniversary|The.Uncut|Ultimate|Final(?=(.(Cut|Edition|Version)))|Extended|Rogue|Special|Despecialized|\d{2,3}(th)?.Anniversary)(.(Cut|Edition|Version))?(.(Extended|Uncensored|Remastered|Unrated|Uncut|IMAX|Fan.?Edit))?|((Uncensored|Remastered|Unrated|Uncut|IMAX|Fan.?Edit|Edition|Restored|((2|3|4)in1))))))\b\)?.{1,3}(?<year>(1(8|9)|20)\d{2}(?!p|i|\d+|\]|\W\d+)))+(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex MovieTitleYearRegex1(); + + [GeneratedRegex(@"^(?<title>(?![([]).+?)?(?:(?:[-_\W](?<![)[!]))*\((?<year>(1(8|9)|20)\d{2}(?!p|i|(1(8|9)|20)\d{2}|\]|\W(1(8|9)|20)\d{2})))+", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex MovieTitleYearRegex2(); + + [GeneratedRegex(@"^(?<title>(?![([]).+?)?(?:(?:[-_\W](?<![)[!]))*(?<year>(1(8|9)|20)\d{2}(?!p|i|(1(8|9)|20)\d{2}|\]|\W(1(8|9)|20)\d{2})))+(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex MovieTitleYearRegex3(); + + [GeneratedRegex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![()[!]))*(?<year>(\[\w *\])))+(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex MovieTitleYearRegex4(); + + [GeneratedRegex(@"^(?<title>(?![([]).+?)?(?:(?:[-_\W](?<![)!]))*(?<year>(1(8|9)|20)\d{2}(?!p|i|\d+|\W\d+)))+(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex MovieTitleYearRegex5(); + + [GeneratedRegex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![)[!]))*(?<year>(1(8|9)|20)\d{2}(?!p|i|\d+|\]|\W\d+)))+(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex MovieTitleYearRegex6(); + + [GeneratedRegex(@"\s*(?:480[ip]|576[ip]|720[ip]|1080[ip]|2160[ip]|HVEC|[xh][\W_]?26[45]|DD\W?5\W1|[<>?*:|]|848x480|1280x720|1920x1080)((8|10)b(it))?", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex SimpleTitleRegex(); + + [GeneratedRegex(@"^\[\s*[a-z]+(\.[a-z]+)+\s*\][- ]*|^www\.[a-z]+\.(?:com|net)[ -]*", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex WebsitePrefixRegex(); + + [GeneratedRegex(@"^\[(?:REQ)\]", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex CleanTorrentPrefixRegex(); + + [GeneratedRegex(@"\[(?:ettv|rartv|rarbg|cttv)\]$", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex CleanTorrentSuffixRegex(); + + [GeneratedRegex(@"\b(Bluray|(dvdr?|BD)rip|HDTV|HDRip|TS|R5|CAM|SCR|(WEB|DVD)?.?SCREENER|DiVX|xvid|web-?dl)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex CommonSourcesRegex(); + + [GeneratedRegex(@"\b(?<webdl>WEB[-_. ]DL|HDRIP|WEBDL|WEB-DLMux|NF|APTV|NETFLIX|NetflixU?HD|DSNY|DSNP|HMAX|AMZN|AmazonHD|iTunesHD|MaxdomeHD|WebHD|WEB$|[. ]WEB[. ](?:[xh]26[45]|DD5[. ]1)|\d+0p[. ]WEB[. ]|\b\s\/\sWEB\s\/\s\b|AMZN[. ]WEB[. ])\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex WebdlExp(); + + [GeneratedRegex(@"\[.+?\]", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex RequestInfoRegex(); + + [GeneratedRegex( + @"\b((Extended.|Ultimate.)?(Director.?s|Collector.?s|Theatrical|Anniversary|The.Uncut|DC|Ultimate|Final(?=(.(Cut|Edition|Version)))|Extended|Special|Despecialized|unrated|\d{2,3}(th)?.Anniversary)(.(Cut|Edition|Version))?(.(Extended|Uncensored|Remastered|Unrated|Uncut|IMAX|Fan.?Edit))?|((Uncensored|Remastered|Unrated|Uncut|IMAX|Fan.?Edit|Edition|Restored|((2|3|4)in1)))){1,3}", + RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex EditionExp(); + + [GeneratedRegex(@"\b(TRUE.?FRENCH|videomann|SUBFRENCH|PLDUB|MULTI)", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex LanguageExp(); + + [GeneratedRegex(@"\b(PROPER|REAL|READ.NFO)", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex SceneGarbageExp(); + + [GeneratedRegex(@"-([a-z0-9]+)$", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex GrouplessTitleRegex(); + + public static void Parse(string title, out string parsedTitle, out string? year) + { + var simpleTitle = SimplifyTitle(title); + + // Removing the group from the end could be trouble if a title is "title-year" + var grouplessTitle = simpleTitle.Replace(GrouplessTitleRegex().ToString(), ""); + + var movieTitleYearRegex = new List<Regex> + { + MovieTitleYearRegex1(), MovieTitleYearRegex2(), MovieTitleYearRegex3(), MovieTitleYearRegex4(), MovieTitleYearRegex5(), + MovieTitleYearRegex6() + }; + + foreach (var exp in movieTitleYearRegex) + { + var match = exp.Match(grouplessTitle); + + if (match.Success) + { + parsedTitle = ReleaseTitleCleaner(match.Groups["title"].Value); + + year = match.Groups["year"].Value; + + return; + } + } + + // year not found, attack using codec or resolution + // attempt to parse using the first found artifact like codec + ResolutionParser.Parse(title, out var resolution, out _); + VideoCodecsParser.Parse(title, out var videoCodec, out _); + AudioChannelsParser.Parse(title, out var channels, out _); + AudioCodecsParser.Parse(title, out var audioCodec, out _); + var resolutionPosition = title.IndexOf(resolution.Value ?? string.Empty, StringComparison.Ordinal); + var videoCodecPosition = title.IndexOf(videoCodec.Value ?? string.Empty, StringComparison.Ordinal); + var channelsPosition = title.IndexOf(channels.Value ?? string.Empty, StringComparison.Ordinal); + var audioCodecPosition = title.IndexOf(audioCodec.Value ?? string.Empty, StringComparison.Ordinal); + var positions = new List<int> {resolutionPosition, audioCodecPosition, channelsPosition, videoCodecPosition}.Where(x => x > 0).ToList(); + + if (positions.Count != 0) + { + var firstPosition = positions.Min(); + parsedTitle = ReleaseTitleCleaner(title[..firstPosition]) ?? string.Empty; + year = null; + return; + } + + parsedTitle = title.Trim(); + year = null; + } + + public static string SimplifyTitle(string title) + { + var simpleTitle = title.Replace(SimpleTitleRegex().ToString(), ""); + simpleTitle = simpleTitle.Replace(WebsitePrefixRegex().ToString(), ""); + simpleTitle = simpleTitle.Replace(CleanTorrentPrefixRegex().ToString(), ""); + simpleTitle = simpleTitle.Replace(CleanTorrentSuffixRegex().ToString(), ""); + simpleTitle = simpleTitle.Replace(CommonSourcesRegex().ToString(), ""); + simpleTitle = simpleTitle.Replace(WebdlExp().ToString(), ""); + + // allow filtering of up to two codecs. + // maybe parseVideoCodec should be an array + VideoCodecsParser.Parse(simpleTitle, out _, out var source1); + + if (!string.IsNullOrEmpty(source1)) + { + simpleTitle = simpleTitle.Replace(source1, ""); + } + + VideoCodecsParser.Parse(simpleTitle, out _, out var source2); + + if (!string.IsNullOrEmpty(source2)) + { + simpleTitle = simpleTitle.Replace(source2, ""); + } + + return simpleTitle.Trim(); + } + + public static string ReleaseTitleCleaner(string title) + { + if (string.IsNullOrEmpty(title) || title.Length == 0 || title == "(") + { + return null; + } + + var trimmedTitle = title.Replace("_", " "); + trimmedTitle = trimmedTitle.Replace(RequestInfoRegex().ToString(), "").Trim(); + trimmedTitle = trimmedTitle.Replace(CommonSourcesRegex().ToString(), "").Trim(); + trimmedTitle = trimmedTitle.Replace(WebdlExp().ToString(), "").Trim(); + trimmedTitle = trimmedTitle.Replace(EditionExp().ToString(), "").Trim(); + trimmedTitle = trimmedTitle.Replace(LanguageExp().ToString(), "").Trim(); + trimmedTitle = trimmedTitle.Replace(SceneGarbageExp().ToString(), "").Trim(); + + foreach (var lang in Enum.GetValues(typeof(Language)).Cast<Language>()) + { + trimmedTitle = trimmedTitle.Replace($@"\b{lang.ToString().ToUpper()}", "").Trim(); + } + + // Look for gap formed by removing items + trimmedTitle = trimmedTitle.Split(" ")[0]; + trimmedTitle = trimmedTitle.Split("..")[0]; + + var parts = trimmedTitle.Split('.'); + var result = ""; + var n = 0; + var previousAcronym = false; + var nextPart = ""; + + foreach (var part in parts) + { + if (parts.Length >= n + 2) + { + nextPart = parts[n + 1] ?? ""; + } + + if (part.Length == 1 && part.ToLower() != "a" && !int.TryParse(part, out _)) + { + result += part + "."; + previousAcronym = true; + } + else if (part.ToLower() == "a" && (previousAcronym || nextPart.Length == 1)) + { + result += part + "."; + previousAcronym = true; + } + else + { + if (previousAcronym) + { + result += " "; + previousAcronym = false; + } + + result += part + " "; + } + + n++; + } + + return result.Trim(); + } +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/TorrentTitleParser.cs b/src/producer/Features/ParseTorrentTitle/TorrentTitleParser.cs new file mode 100644 index 0000000..c42fa9d --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/TorrentTitleParser.cs @@ -0,0 +1,96 @@ +namespace Producer.Features.ParseTorrentTitle; + +public static class TorrentTitleParser +{ + public static ParsedFilename Parse(string name, bool isTv = false) + { + VideoCodecsParser.Parse(name, out var videoCodec, out _); + AudioCodecsParser.Parse(name, out var audioCodec, out _); + AudioChannelsParser.Parse(name, out var audioChannels, out _); + LanguageParser.Parse(name, out var languages); + QualityParser.Parse(name, out var quality); + var group = GroupParser.Parse(name); + var edition = EditionParser.Parse(name); + var multi = LanguageParser.IsMulti(name); + var complete = Complete.IsComplete(name); + + var baseParsed = new BaseParsed + { + Resolution = quality.Resolution, + Sources = quality.Sources, + VideoCodec = videoCodec, + AudioCodec = audioCodec, + AudioChannels = audioChannels, + Revision = quality.Revision, + Group = group, + Edition = edition, + Languages = languages, + Multi = multi, + Complete = complete, + }; + + return !isTv ? ParseMovie(name, baseParsed) : ParseSeason(name, baseParsed); + } + + private static ParsedFilename ParseSeason(string name, BaseParsed baseParsed) + { + var season = SeasonParser.Parse(name); + + return new() + { + Show = new() + { + EpisodeNumbers = season.EpisodeNumbers, + FullSeason = season.FullSeason, + IsPartialSeason = season.IsPartialSeason, + IsSpecial = season.IsSpecial, + SeasonPart = season.SeasonPart, + IsSeasonExtra = season.IsSeasonExtra, + SeriesTitle = season.SeriesTitle, + IsMultiSeason = season.IsMultiSeason, + AirDate = season.AirDate, + Seasons = season.Seasons, + ReleaseTitle = season.ReleaseTitle, + Edition = baseParsed.Edition, + Resolution = baseParsed.Resolution, + Sources = baseParsed.Sources, + VideoCodec = baseParsed.VideoCodec, + Complete = baseParsed.Complete, + AudioCodec = baseParsed.AudioCodec, + Languages = baseParsed.Languages, + AudioChannels = baseParsed.AudioChannels, + Group = baseParsed.Group, + Multi = baseParsed.Multi, + Revision = baseParsed.Revision, + }, + }; + } + + private static ParsedFilename ParseMovie(string name, BaseParsed baseParsed) + { + TitleParser.Parse(name, out var title, out var year); + + baseParsed.Title = title; + baseParsed.Year = year; + + return new() + { + Movie = new() + { + Title = baseParsed.Title, + Year = baseParsed.Year, + Edition = baseParsed.Edition, + Resolution = baseParsed.Resolution, + Sources = baseParsed.Sources, + VideoCodec = baseParsed.VideoCodec, + Complete = baseParsed.Complete, + AudioCodec = baseParsed.AudioCodec, + Languages = baseParsed.Languages, + AudioChannels = baseParsed.AudioChannels, + Group = baseParsed.Group, + Multi = baseParsed.Multi, + Revision = baseParsed.Revision, + }, + }; + } +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/VideoCodecs.cs b/src/producer/Features/ParseTorrentTitle/VideoCodecs.cs new file mode 100644 index 0000000..a5c901b --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/VideoCodecs.cs @@ -0,0 +1,16 @@ +namespace Producer.Features.ParseTorrentTitle; + +public sealed class VideoCodec : SmartEnum<VideoCodec, string> +{ + public static readonly VideoCodec X265 = new("X265", "x265"); + public static readonly VideoCodec X264 = new("X264", "x264"); + public static readonly VideoCodec H264 = new("H264", "h264"); + public static readonly VideoCodec H265 = new("H265", "h265"); + public static readonly VideoCodec WMV = new("WMV", "WMV"); + public static readonly VideoCodec XVID = new("XVID", "xvid"); + public static readonly VideoCodec DVDR = new("DVDR", "dvdr"); + + private VideoCodec(string name, string value) : base(name, value) + { + } +} \ No newline at end of file diff --git a/src/producer/Features/ParseTorrentTitle/VideoCodecsParser.cs b/src/producer/Features/ParseTorrentTitle/VideoCodecsParser.cs new file mode 100644 index 0000000..ad7f5f8 --- /dev/null +++ b/src/producer/Features/ParseTorrentTitle/VideoCodecsParser.cs @@ -0,0 +1,89 @@ +namespace Producer.Features.ParseTorrentTitle; + +public partial class VideoCodecsParser +{ + [GeneratedRegex(@"(?<x265>x265)", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex X265Exp(); + + [GeneratedRegex(@"(?<h265>h265)", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex H265Exp(); + + [GeneratedRegex(@"(?<x264>x264)", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex X264Exp(); + + [GeneratedRegex(@"(?<h264>h264)", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex H264Exp(); + + [GeneratedRegex(@"(?<wmv>WMV)", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex WMVExp(); + + [GeneratedRegex(@"(?<xvidhd>XvidHD)", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex XvidhdExp(); + + [GeneratedRegex(@"(?<xvid>X-?vid)", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex XvidExp(); + + [GeneratedRegex(@"(?<divx>divx)", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex DivxExp(); + + [GeneratedRegex(@"(?<hevc>HEVC)", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex HevcExp(); + + [GeneratedRegex(@"(?<dvdr>DVDR)\b", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex DvdrExp(); + + private static readonly Regex CodecExp = new( + string.Join( + "|", X265Exp(), H265Exp(), X264Exp(), H264Exp(), WMVExp(), XvidhdExp(), XvidExp(), DivxExp(), HevcExp(), DvdrExp()), RegexOptions.IgnoreCase); + + public static void Parse(string title, out VideoCodec? codec, out string? source) + { + codec = null; + source = null; + + var result = CodecExp.Match(title); + + if (!result.Success) + { + return; + } + + var groups = result.Groups; + + if (groups["h264"].Success) + { + codec = VideoCodec.H264; + source = groups["h264"].Value; + } + else if (groups["h265"].Success) + { + codec = VideoCodec.H265; + source = groups["h265"].Value; + } + else if (groups["x265"].Success || groups["hevc"].Success) + { + codec = VideoCodec.X265; + source = groups["x265"].Success ? groups["x265"].Value : groups["hevc"].Value; + } + else if (groups["x264"].Success) + { + codec = VideoCodec.X264; + source = groups["x264"].Value; + } + else if (groups["xvidhd"].Success || groups["xvid"].Success || groups["divx"].Success) + { + codec = VideoCodec.XVID; + source = groups["xvidhd"].Success ? groups["xvidhd"].Value : (groups["xvid"].Success ? groups["xvid"].Value : groups["divx"].Value); + } + else if (groups["wmv"].Success) + { + codec = VideoCodec.WMV; + source = groups["wmv"].Value; + } + else if (groups["dvdr"].Success) + { + codec = VideoCodec.DVDR; + source = groups["dvdr"].Value; + } + } +} \ No newline at end of file diff --git a/src/producer/Features/Text/AdultContentConfiguration.cs b/src/producer/Features/Text/AdultContentConfiguration.cs new file mode 100644 index 0000000..968ff5d --- /dev/null +++ b/src/producer/Features/Text/AdultContentConfiguration.cs @@ -0,0 +1,14 @@ +namespace Producer.Features.Text; + +public class AdultContentConfiguration +{ + public const string SectionName = "AdultContentSettings"; + public const string Filename = "adultcontent.json"; + + public bool Allow { get; set; } + + public List<string> Keywords { get; set; } = []; + public List<string> CompoundKeywords { get; set; } = []; + + public int Threshold { get; set; } +} \ No newline at end of file diff --git a/src/producer/Features/Text/FuzzySearcher.cs b/src/producer/Features/Text/FuzzySearcher.cs new file mode 100644 index 0000000..83c373d --- /dev/null +++ b/src/producer/Features/Text/FuzzySearcher.cs @@ -0,0 +1,13 @@ +namespace Producer.Features.Text; + +public class FuzzyStringSearcher(IEnumerable<string> records, SearchOptions<string>? options = null) : IFuzzySearcher<string> +{ + private readonly IReadOnlyCollection<string> _records = records.ToList(); + private readonly SearchOptions<string> _options = options ?? new SearchOptions<string>(); + + public IReadOnlyCollection<ExtractedResult<string>> Search(string text) + { + var dynamicThreshold = (int) Math.Ceiling(text.Length * (_options.Threshold / 100.0)); + return Process.ExtractSorted(text, _records, cutoff: dynamicThreshold).ToList(); + } +} diff --git a/src/producer/Features/Text/IFuzzySearcher.cs b/src/producer/Features/Text/IFuzzySearcher.cs new file mode 100644 index 0000000..d5f0a2a --- /dev/null +++ b/src/producer/Features/Text/IFuzzySearcher.cs @@ -0,0 +1,6 @@ +namespace Producer.Features.Text; + +public interface IFuzzySearcher<T> +{ + IReadOnlyCollection<ExtractedResult<T>> Search(string text); +} \ No newline at end of file diff --git a/src/producer/Features/Text/SearchOptions.cs b/src/producer/Features/Text/SearchOptions.cs new file mode 100644 index 0000000..d5cc1d0 --- /dev/null +++ b/src/producer/Features/Text/SearchOptions.cs @@ -0,0 +1,7 @@ +namespace Producer.Features.Text; + +public class SearchOptions<T> +{ + public int Threshold { get; init; } = 60; + +} \ No newline at end of file diff --git a/src/producer/Features/Text/SearchResultRecords.cs b/src/producer/Features/Text/SearchResultRecords.cs new file mode 100644 index 0000000..32c0438 --- /dev/null +++ b/src/producer/Features/Text/SearchResultRecords.cs @@ -0,0 +1,16 @@ +namespace Producer.Features.Text; + +public class SearchResultRecords +{ + public record struct ScoreInfo(int Errors, int CurrentLocation, int ExpectedLocation, int Distance, + bool IgnoreLocation); + + public record struct SearchResult(bool IsMatch, double Score); + + public record struct Index(List<Chunk> Chunks, string Pattern); + + public record struct Chunk(int StartIndex, string Pattern, Dictionary<char, int> Alphabet); + + public record struct SearchResult<T>(T Value, double Score); +} + diff --git a/src/producer/Features/Text/ServiceCollectionExtensions.cs b/src/producer/Features/Text/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..2178f9e --- /dev/null +++ b/src/producer/Features/Text/ServiceCollectionExtensions.cs @@ -0,0 +1,26 @@ +namespace Producer.Features.Text; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection RegisterAdultKeywordFilter(this IServiceCollection services, IConfiguration configuration) + { + var adultConfigSettings = + services.LoadConfigurationFromConfig<AdultContentConfiguration>(configuration, AdultContentConfiguration.SectionName); + + if (adultConfigSettings.Allow) + { + return services; + } + + return services.AddSingleton<IFuzzySearcher<string>>( + _ => + { + var options = new SearchOptions<string> + { + Threshold = adultConfigSettings.Threshold, + }; + + return new FuzzyStringSearcher(adultConfigSettings.Keywords, options); + }); + } +} \ No newline at end of file diff --git a/src/producer/GlobalUsings.cs b/src/producer/GlobalUsings.cs index a9de3af..ce8dea6 100644 --- a/src/producer/GlobalUsings.cs +++ b/src/producer/GlobalUsings.cs @@ -1,12 +1,17 @@ // Global using directives +global using System.Globalization; global using System.Reflection; global using System.Text; global using System.Text.Json; global using System.Text.RegularExpressions; global using System.Threading.Channels; global using System.Xml.Linq; +global using Ardalis.SmartEnum; global using Dapper; +global using FuzzySharp; +global using FuzzySharp.Extractor; +global using FuzzySharp.PreProcess; global using LZStringCSharp; global using MassTransit; global using Microsoft.AspNetCore.Builder; @@ -25,4 +30,6 @@ global using Producer.Features.Crawlers.Torrentio; global using Producer.Features.CrawlerSupport; global using Producer.Features.DataProcessing; global using Producer.Features.JobSupport; +global using Producer.Features.ParseTorrentTitle; +global using Producer.Features.Text; global using Serilog; diff --git a/src/producer/Producer.csproj b/src/producer/Producer.csproj index b85b0ee..c6b0e13 100644 --- a/src/producer/Producer.csproj +++ b/src/producer/Producer.csproj @@ -9,7 +9,9 @@ </PropertyGroup> <ItemGroup> + <PackageReference Include="Ardalis.SmartEnum" Version="8.0.0" /> <PackageReference Include="Dapper" Version="2.1.28" /> + <PackageReference Include="FuzzySharp" Version="2.0.2" /> <PackageReference Include="LZStringCSharp" Version="1.4.0" /> <PackageReference Include="MassTransit" Version="8.1.3" /> <PackageReference Include="MassTransit.RabbitMQ" Version="8.1.3" /> diff --git a/src/producer/Program.cs b/src/producer/Program.cs index 280431a..4b5ca46 100644 --- a/src/producer/Program.cs +++ b/src/producer/Program.cs @@ -10,6 +10,7 @@ builder.Services .RegisterMassTransit() .AddDataStorage() .AddCrawlers() + .RegisterAdultKeywordFilter(builder.Configuration) .AddQuartz(builder.Configuration); var host = builder.Build();