* lots of testing & improvement for movie auto-detection

* more robust CLI movie-mode
This commit is contained in:
Reinhard Pointner 2012-06-22 07:47:26 +00:00
parent d4b38f918f
commit a5284ccb40
10 changed files with 232 additions and 53 deletions

View File

@ -26,7 +26,7 @@ tsv.text.eachLine{
}
}
movies = movies.findAll{ it[0] <= 9999999 && it[2] >= 1960 && it[1] =~ /^[A-Z0-9]/ && it[1] =~ /[\p{Alpha}]{3}/ }.sort{ it[1] }
movies = movies.findAll{ it[0] <= 9999999 && it[2] >= 1930 && it[1] =~ /^[A-Z0-9]/ && it[1] =~ /[\p{Alpha}]{3}/ }.sort{ it[1] }
gz(m_out, movies.collect{ [it[0].pad(7), it[1], it[2]].join('\t') })
println "Movie Count: " + movies.size()

View File

@ -313,8 +313,9 @@ public class CmdlineOperations implements CmdlineInterface {
derivatesByMovieFile.put(movieFile, new ArrayList<File>());
}
for (File file : orphanedFiles) {
List<File> orphanParent = listPath(file);
for (File movieFile : movieFiles) {
if (isDerived(file, movieFile)) {
if (orphanParent.contains(movieFile.getParentFile()) && isDerived(file, movieFile)) {
derivatesByMovieFile.get(movieFile).add(file);
break;
}
@ -382,7 +383,11 @@ public class CmdlineOperations implements CmdlineInterface {
if (movie == null) {
CLILogger.fine(format("Auto-detect movie from context: [%s]", file));
Collection<Movie> results = detectMovie(file, null, service, locale, strict);
movie = (Movie) selectSearchResult(query, results, strict).get(0);
try {
movie = (Movie) selectSearchResult(query, results, strict).get(0);
} catch (Exception e) {
CLILogger.log(Level.WARNING, String.format("%s: [%s/%s] %s", e.getClass().getSimpleName(), guessMovieFolder(file) != null ? guessMovieFolder(file).getName() : null, file.getName(), e.getMessage()));
}
if (movie != null) {
Analytics.trackEvent(service.getName(), "SearchMovie", movie.toString(), 1);

View File

@ -15,6 +15,7 @@ import java.net.MalformedURLException;
import java.net.URL;
import java.text.CollationKey;
import java.text.Collator;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
@ -301,19 +302,13 @@ public class MediaDetection {
// search by file name or folder name
List<String> terms = new ArrayList<String>();
// 1. term: try to match movie pattern 'name (year)' or use filename as is
Matcher nameMatcher = compile("^(.+?)[(]((?:19|20)\\d{2})[)]").matcher(movieFile.getName());
if (nameMatcher.find()) {
terms.add(String.format("%s (%s)", nameMatcher.group(1).trim(), nameMatcher.group(2)));
} else {
terms.add(getName(movieFile));
}
terms.add(reduceMovieName(getName(movieFile)));
// 2. term: first meaningful parent folder
File movieFolder = guessMovieFolder(movieFile);
if (movieFolder != null) {
terms.add(getName(movieFolder));
terms.add(reduceMovieName(getName(movieFolder)));
}
List<Movie> movieNameMatches = matchMovieName(terms, locale, strict);
@ -329,6 +324,11 @@ public class MediaDetection {
movieNameMatches = matchMovieName(terms, locale, false);
}
// assume name without spacing will mess up any lookup
if (movieNameMatches.isEmpty()) {
movieNameMatches = matchMovieFromStringWithoutSpacing(terms, new NameSimilarityMetric(), strict ? 0.9f : 0.6f);
}
// query by file / folder name
if (queryLookupService != null) {
options.addAll(queryMovieByFileName(terms, queryLookupService, locale));
@ -344,6 +344,15 @@ public class MediaDetection {
}
public static String reduceMovieName(String name) throws IOException {
Matcher reluctantMatcher = compile("^(.+)[\\[\\(]((?:19|20)\\d{2})[\\]\\)]").matcher(name);
if (reluctantMatcher.find()) {
return String.format("%s %s", reluctantMatcher.group(1).trim(), reluctantMatcher.group(2));
}
return name;
}
public static File guessMovieFolder(File movieFile) throws IOException {
// first meaningful parent folder (max 2 levels deep)
File f = movieFile.getParentFile();
@ -357,24 +366,39 @@ public class MediaDetection {
}
private static List<Movie> matchMovieName(final List<String> files, final Locale locale, final boolean strict) throws Exception {
private static List<Entry<String, Movie>> movieIndex;
private static synchronized List<Entry<String, Movie>> getMovieIndex() throws IOException {
if (movieIndex == null) {
Movie[] movies = releaseInfo.getMovieList();
movieIndex = new ArrayList<Entry<String, Movie>>(movies.length);
for (Movie movie : movies) {
movieIndex.add(new SimpleEntry<String, Movie>(normalizePunctuation(movie.getName()).toLowerCase(), movie));
}
}
return movieIndex;
}
public static List<Movie> matchMovieName(final List<String> files, final Locale locale, final boolean strict) throws Exception {
// cross-reference file / folder name with movie list
final HighPerformanceMatcher nameMatcher = new HighPerformanceMatcher(3);
final Map<Movie, String> matchMap = new HashMap<Movie, String>();
for (final Movie movie : releaseInfo.getMovieList()) {
for (Entry<String, Movie> movie : getMovieIndex()) {
for (String name : files) {
String movieIdentifier = movie.getName();
String movieIdentifier = movie.getKey();
String commonName = nameMatcher.matchFirstCommonSequence(name, movieIdentifier);
if (commonName != null && commonName.length() >= movieIdentifier.length()) {
String strictMovieIdentifier = movie.getName() + " " + movie.getYear();
String strictMovieIdentifier = movie.getKey() + " " + movie.getValue().getYear();
String strictCommonName = nameMatcher.matchFirstCommonSequence(name, strictMovieIdentifier);
if (strictCommonName != null && strictCommonName.length() >= strictMovieIdentifier.length()) {
// prefer strict match
matchMap.put(movie, strictCommonName);
matchMap.put(movie.getValue(), strictCommonName);
} else if (!strict) {
// make sure the common identifier is not just the year
matchMap.put(movie, commonName);
matchMap.put(movie.getValue(), commonName);
}
}
}
@ -394,6 +418,32 @@ public class MediaDetection {
}
public static List<Movie> matchMovieFromStringWithoutSpacing(List<String> files, SimilarityMetric metric, float similarityThreshold) throws IOException {
Pattern spacing = Pattern.compile("[\\p{Punct}\\p{Space}]+");
List<String> terms = new ArrayList<String>(files.size());
for (String it : files) {
String term = spacing.matcher(it).replaceAll("").toLowerCase();
if (term.length() >= 3) {
terms.add(term); // only consider words, not just random letters
}
}
List<Movie> movies = new ArrayList<Movie>();
for (Entry<String, Movie> it : getMovieIndex()) {
String name = spacing.matcher(it.getKey()).replaceAll("").toLowerCase();
for (String term : terms) {
if (term.contains(name) && metric.getSimilarity(name, term) >= similarityThreshold) {
movies.add(it.getValue());
break;
}
}
}
return movies;
}
private static Collection<Movie> queryMovieByFileName(List<String> files, MovieIdentificationService queryLookupService, Locale locale) throws Exception {
// remove blacklisted terms
Set<String> querySet = new LinkedHashSet<String>();

View File

@ -3,6 +3,7 @@ package net.sourceforge.filebot.media;
import static java.util.Arrays.*;
import static java.util.Collections.*;
import static java.util.ResourceBundle.*;
import static java.util.regex.Pattern.*;
import static net.sourceforge.filebot.similarity.Normalization.*;
@ -28,6 +29,7 @@ import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeMap;
import java.util.WeakHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
@ -93,6 +95,7 @@ public class ReleaseInfo {
public List<String> cleanRelease(Collection<String> items, boolean strict) throws IOException {
Set<String> languages = getLanguageMap(Locale.ENGLISH, Locale.getDefault()).keySet();
Pattern clutterBracket = getClutterBracketPattern(strict);
Pattern releaseGroup = getReleaseGroupPattern(strict);
Pattern languageSuffix = getLanguageSuffixPattern(languages);
Pattern languageTag = getLanguageTagPattern(languages);
@ -101,8 +104,8 @@ public class ReleaseInfo {
Pattern resolution = getResolutionPattern();
Pattern queryBlacklist = getBlacklistPattern();
Pattern[] blacklist = new Pattern[] { releaseGroup, languageSuffix, languageTag, videoSource, videoFormat, resolution, queryBlacklist };
Pattern[] stopwords = new Pattern[] { getReleaseGroupPattern(true), languageSuffix, languageTag, videoSource, videoFormat, resolution };
Pattern[] stopwords = new Pattern[] { getReleaseGroupPattern(true), languageTag, videoSource, videoFormat, resolution, languageSuffix };
Pattern[] blacklist = new Pattern[] { clutterBracket, releaseGroup, languageTag, videoSource, videoFormat, resolution, languageSuffix, queryBlacklist };
List<String> output = new ArrayList<String>(items.size());
for (String it : items) {
@ -132,11 +135,9 @@ public class ReleaseInfo {
for (Pattern it : stopwords) {
Matcher matcher = it.matcher(item);
if (matcher.find()) {
return item.substring(0, matcher.start()); // use substring before the matched stopword
item = item.substring(0, matcher.start()); // use substring before the matched stopword
}
}
// no stopword found, keep original string
return item;
}
@ -149,7 +150,7 @@ public class ReleaseInfo {
public Pattern getLanguageSuffixPattern(Collection<String> languages) {
// .en.srt
return compile("(?<=\\p{Punct}|\\s)(" + join(quoteAll(languages), "|") + ")(?=$)", CASE_INSENSITIVE | UNICODE_CASE | CANON_EQ);
return compile("(?<=[\\p{Punct}\\p{Space}])(" + join(quoteAll(languages), "|") + ")(?=[._ ]*$)", CASE_INSENSITIVE | UNICODE_CASE | CANON_EQ);
}
@ -173,6 +174,13 @@ public class ReleaseInfo {
}
public Pattern getClutterBracketPattern(boolean strict) {
// match patterns like [Action, Drama] or {ENG-XViD-MP3-DVDRiP} etc
String contentFilter = strict ? "[\\p{Space}\\p{Punct}&&[^\\[\\]]]" : "\\p{Alpha}";
return compile("(?:\\[([^\\[\\]]+?" + contentFilter + "[^\\[\\]]+?)\\])|(?:\\{([^\\{\\}]+?" + contentFilter + "[^\\{\\}]+?)\\})|(?:\\(([^\\(\\)]+?" + contentFilter + "[^\\(\\)]+?)\\))");
}
public synchronized Pattern getReleaseGroupPattern(boolean strict) throws IOException {
// pattern matching any release group name enclosed in separators
return compile("(?<!\\p{Alnum})(" + join(releaseGroupResource.get(), "|") + ")(?!\\p{Alnum})", strict ? 0 : CASE_INSENSITIVE | UNICODE_CASE | CANON_EQ);
@ -314,7 +322,17 @@ public class ReleaseInfo {
}
private final Map<Set<Locale>, Map<String, Locale>> languageMapCache = synchronizedMap(new WeakHashMap<Set<Locale>, Map<String, Locale>>(2));
private Map<String, Locale> getLanguageMap(Locale... supportedDisplayLocale) {
// try cache
Set<Locale> displayLocales = new HashSet<Locale>(asList(supportedDisplayLocale));
Map<String, Locale> languageMap = languageMapCache.get(displayLocales);
if (languageMap != null) {
return languageMap;
}
// use maximum strength collator by default
Collator collator = Collator.getInstance(Locale.ROOT);
collator.setDecomposition(Collator.FULL_DECOMPOSITION);
@ -322,9 +340,7 @@ public class ReleaseInfo {
@SuppressWarnings("unchecked")
Comparator<String> order = (Comparator) collator;
Map<String, Locale> languageMap = new TreeMap<String, Locale>(order);
Set<Locale> displayLocales = new HashSet<Locale>(asList(supportedDisplayLocale));
languageMap = new TreeMap<String, Locale>(order);
for (String code : Locale.getISOLanguages()) {
Locale locale = new Locale(code);
@ -341,7 +357,11 @@ public class ReleaseInfo {
// remove illegal tokens
languageMap.remove("");
return languageMap;
languageMap.remove("II");
languageMap.remove("III");
Map<String, Locale> result = unmodifiableMap(languageMap);
languageMapCache.put(displayLocales, result);
return result;
}
}

View File

@ -1,5 +1,5 @@
# source names mostly copied from [http://en.wikipedia.org/wiki/Pirated_movie_release_types]
pattern.video.source: CAMRip|CAM|PDVD|TS|TELESYNC|PDVD|PPV|PPVRip|Screener|SCR|SCREENER|DVDSCR|DVDSCREENER|BDSCR|R5|R5LINE|DVD|DVDRip|DVDR|TVRip|DSR|PDTV|HDTV|DVB|DVBRip|DTHRip|VODRip|VODR|BDRip|BRRip|BluRay|BDR|BR-Scr|BR-Screener|HDDVD|HDRip|WorkPrint|VHS|VCD|TELECINE|WEB-DL|Webrip
pattern.video.source: CAMRip|CAM|PDVD|TS|TELESYNC|PDVD|PPV|PPVRip|Screener|SCR|SCREENER|DVDSCR|DVDSCREENER|BDSCR|R4|R5|R5LINE|R5.LINE|DVD|DVDRip|DVDR|TVRip|DSR|PDTV|HDTV|DVB|DVBRip|DTHRip|VODRip|VODR|BDRip|BRRip|BluRay|BDR|BR-Scr|BR-Screener|HDDVD|HDRip|WorkPrint|VHS|VCD|TELECINE|WEB-DL|WEBRip
# additional release info patterns
pattern.video.format: DivX|Xvid|AVC|x264|h264|3ivx|mpeg|mpeg4|mp3|aac|ac3|2ch|6ch|WS|HR|720p|1080p|NTSC

View File

@ -236,10 +236,12 @@ class MovieHashMatcher implements AutoCompleteMatcher {
String input = null;
synchronized (inputMemory) {
input = inputMemory.get(suggestion);
if (input == null || suggestion == null || suggestion.isEmpty()) {
input = showInputDialog("Enter movie name:", suggestion, String.format("%s/%s", movieFile.getParentFile().getName(), movieFile.getName()), parent);
inputMemory.put(suggestion, input);
synchronized (this) {
input = inputMemory.get(suggestion);
if (input == null || suggestion == null || suggestion.isEmpty()) {
input = showInputDialog("Enter movie name:", suggestion, String.format("%s/%s", movieFile.getParentFile().getName(), movieFile.getName()), parent);
inputMemory.put(suggestion, input);
}
}
}
@ -320,8 +322,8 @@ class MovieHashMatcher implements AutoCompleteMatcher {
});
// allow only one select dialog at a time
synchronized (this) {
synchronized (selectionMemory) {
synchronized (selectionMemory) {
synchronized (this) {
if (selectionMemory.containsKey(fileQuery)) {
return selectionMemory.get(fileQuery);
}

View File

@ -146,7 +146,7 @@ public class OpenSubtitlesXmlRpc {
movies.add(new Movie(name, year, Integer.parseInt(imdbid)));
} catch (Exception e) {
Logger.getLogger(OpenSubtitlesXmlRpc.class.getName()).log(Level.INFO, String.format("Ignore movie %s: %s", movie, e.getMessage()));
Logger.getLogger(OpenSubtitlesXmlRpc.class.getName()).log(Level.INFO, String.format("Ignore movie [%s]: %s", movie, e.getMessage()));
}
}

View File

@ -41,8 +41,8 @@ public class TMDbClient implements MovieIdentificationService {
private static final String host = "api.themoviedb.org";
private static final String version = "2.1";
private static final FloodLimit SEARCH_LIMIT = new FloodLimit(10, 10, TimeUnit.SECONDS);
private static final FloodLimit REQUEST_LIMIT = new FloodLimit(20, 10, TimeUnit.SECONDS);
private static final FloodLimit SEARCH_LIMIT = new FloodLimit(10, 12, TimeUnit.SECONDS);
private static final FloodLimit REQUEST_LIMIT = new FloodLimit(30, 12, TimeUnit.SECONDS);
private final String apikey;
@ -114,18 +114,27 @@ public class TMDbClient implements MovieIdentificationService {
List<Movie> result = new ArrayList<Movie>();
for (Node node : selectNodes("//movie", dom)) {
String name = getTextContent("name", node);
try {
String name = getTextContent("name", node);
// release date format will be YYYY-MM-DD, but we only care about the year
int year = new Scanner(getTextContent("released", node)).useDelimiter("\\D+").nextInt();
int year = -1;
try {
year = new Scanner(getTextContent("released", node)).useDelimiter("\\D+").nextInt();
} catch (RuntimeException e) {
throw new IllegalArgumentException("Missing data: year");
}
// imdb id will be tt1234567, but we only care about the number
int imdbid = new Scanner(getTextContent("imdb_id", node)).useDelimiter("\\D+").nextInt();
int imdbid = -1;
try {
imdbid = new Scanner(getTextContent("imdb_id", node)).useDelimiter("\\D+").nextInt();
} catch (RuntimeException e) {
throw new IllegalArgumentException("Missing data: imdbid");
}
result.add(new Movie(name, year, imdbid));
} catch (RuntimeException e) {
// release date or imdb id are undefined
} catch (Exception e) {
Logger.getLogger(TMDbClient.class.getName()).log(Level.INFO, String.format("Ignore movie [%s]: %s", name, e.getMessage()));
}
}

View File

@ -1,7 +1,18 @@
(?-i:CLASSiC)
(?-i:DOCU)
(?-i:ENGLISH)
(?-i:FIXED)
(?-i:FRENCH)
(?-i:GERMAN)
(?-i:iNT)
(?-i:LIMITED|LiMiTED)
(?-i:SPANISH)
(?-i:SWEDISH|SWEDiSH)
.+sample$
1-3-3-8.com
5[.,]1
@KIDZ
[1-3]CD
[1-3]CDRip
[1-9].?of.?[1-9]
^(TV.)?(Show|Serie)[s]?
@ -13,6 +24,7 @@
^Film[s]?
^HVDVD_TS$
^Info
^l[^\p{Alnum}]
^Movie[s]?
^New$
^SAMPLE
@ -28,24 +40,28 @@ By.Cool.Release
CD[0]?[1-3]
CN
CVCD
DC
Demonoid
Director's.Cut
Directors.Cut
Dual.Audio
dubbed
DVDXvID
DVSKY
ENG
ENGLISH
EXTENDED
Extended.Version
ExtraScene
ExtraTorrent
Final.Cut
Fra
FRE
FRENCH
GER
GERMAN
Hard.Subbed
HDRip
Hindi
HQ
iNTERNAL
iPod
ISO
iTA
@ -53,9 +69,11 @@ iTALIA
jigaxx
KIDZCORNER
KOR
KORSUB
LMAO
Los.Sustitutos
mkvonly
Movie[s]?
MultiSub
MVGroup.org
NL
NL.Subs
@ -65,9 +83,12 @@ PROPER
PSP
READNFO
REAL.PROPER
REMASTERED
REPACK
ReRip
RESYNC
RETAIL
RiffTrax
Sample
sample[s]?$
Screenshot
@ -77,7 +98,7 @@ ShareZONE
ShortKut
Snapshots
SPA
SPANISH
Special.Edition
Sub
SUBBED
Subs
@ -87,10 +108,12 @@ swe.?sub
SYNC
SYNCFIX
TC
theatrical.cut
TPB
TRUEFRENCH
TS
TSXVID
ultimate.edition
UNCUT
unrated
unrated.edition

View File

@ -9,6 +9,7 @@
2WIRE
310yuma
3Li
3LT0N
420RipZ
4HM
7SiNS
@ -18,6 +19,7 @@ aacrime
aAF
AaS
aBD
AbSurdity
aceford
ADHD
AE
@ -32,14 +34,19 @@ AKUPX
ALANiS
ALeSiO
ALLiANCE
ALLZINE
AMiABLE
AN0NYM0US
aNBc
ANBU
Ani-Kraze
ANiHLS
AonE
ARiGOLD
ARROW
ArtSubs
ASAP
ATTENTATET
AVCHD
AVS720
AW
@ -56,6 +63,7 @@ bc10
BDClub
BDiSC
beAst
BeStDivX
BestHD
BiA
BiDA
@ -69,6 +77,7 @@ BLUEYES
blueZilla
BluWave
BMB
BORGATA
bReAK
BrG
BRiGHT
@ -109,13 +118,13 @@ COALiTiON
Cocksure
COMPULSION
cottage
COWiSO
CPtScene
CPY
CRF
CRIMSON
CRiSC
CROSSBOW
Crow
CRYS
CSHD
CtrlHD
@ -131,15 +140,18 @@ DARM
DASH
DATA
DAW
DCA
DDC
dddc
DEAL
decibeL
DEFACED
DEFiNiTE
DEFiNiTiON
DEFUSED
DEiTY
DEPRAViTY
DEPRiVED
DETAiLS
DEViSE
DEWSTRR
@ -163,6 +175,7 @@ DnB
DNL
DNR
DON
DoNE
DOT
doubt
DOWN
@ -171,6 +184,7 @@ DUPLI
DUQA
DutchReleaseTeam
DvF
DVL
EBi
EbP
ECHiZEN
@ -183,6 +197,7 @@ Electri4ka
ELECTRiC
Electrichka
elizabethtga
EM0C0RE
EmC
EMPiREHD
ENCOUNTERS
@ -190,6 +205,7 @@ EnDoR
eots
EPiK
ESiR
ESPiSE
ETHOS
ETM
ETRG
@ -198,8 +214,11 @@ EuchHD
EUHD
EuReKA
EUSTASS
EwDp
EXiLE
EXQUiSiTE
ExtraTorrentRG
EXViD
eztv
FaNSuB
FASM
@ -210,10 +229,14 @@ FHM
FiCO
FiHTV
FilmHD
FiNaLe
fjall
FLAiTE
Flaket
fLAMEhd
FLAWL3SS
Flomp-Rumbel
FLS
FLX
FmE
ForceBleue
@ -232,6 +255,7 @@ FTVDT
FTW-FM
FTW-HD
fty
FUCT
Funner
FXG
FxM
@ -239,8 +263,10 @@ G3N3
GAGE
Gazdi
GB
GECKOS
GEHENNA
george.c
GFW
GFY
GiNJi
GMoRK
@ -250,6 +276,7 @@ GoLDSToNE
GOTHiC
GriOTS
Grond
gudhak
H2
h264iRMU
H@M
@ -293,6 +320,7 @@ HiFi
HiGHTIMES
HiNT
HoodBag
HORiZON
HOWL
HqDTS
HUBRiS
@ -305,10 +333,13 @@ iaK
iBEX
iCANDY
iGNHD
iGNiTiON
IGUANA
iKA
iLG
iLL
iMAGiNE
iMBT
IMF
IMMERSE
imNaKeD
@ -330,6 +361,7 @@ JAVLiU
JCH
JENC
JJH
JoLLyRoGeR
K-F
k2
KaKa
@ -337,7 +369,9 @@ kamera
keltz
KiNGS
KLAXXON
KlockreN
KOENiG
KonzillaRG
KRaLiMaRKo
KYR
Kyuubi
@ -355,6 +389,7 @@ LOLCATS
LoneWolf
LOST
LP
LTRG
LTT
LUSO
M794
@ -367,6 +402,7 @@ MARiNES
MAXSPEED
MC
MCR
MEDiAMANiACS
MEDiEVAL
MELiTE
MeTH
@ -376,16 +412,20 @@ MiND
MiNT
MiRAGETV
MMI
MoF
MOMENTUM
MONK
MOREHD
MOTU
MOViERUSH
MOViESTARS
MrLore
mSD
MsR
MuSt
mV4U
mVmHD
MXMG
MySiLU
N-F
NaRB
@ -405,8 +445,9 @@ Nile
NiX
NL.Subs
NODLABS
NoGrp
NOHD
Noir
NOiR
NORARS
NoSCR
NOsegmenT
@ -427,12 +468,15 @@ ONYX
ORC
ORENJi
ORPHEUS
OSiRiS
OSiTV
OUTDATED
OZC
P0W4
Pa@Ph
PADDO
papi
PARTiCLE
PaYxXx
PeeWee
PELLUCiD
@ -445,12 +489,15 @@ PiLAF
PiNER
PiX
PixelHD
playXD
POD
PoRNDoCtOR
PORNOHOLiCS
PosTX
PoTuS
PP
PPQ
PRECiOUS
Prime
PriMeHD
PRiNCE
@ -460,10 +507,11 @@ PROPHETS
ProPL
PRXHD
PS3-TEAM
psig
PSiG
PSV
PSYCHD
Pti
PtP
PtS
Pudding
Pukka
@ -474,6 +522,7 @@ PxHD
Q0S
QCF
QDP
QiX
QSP
QXE
R&C
@ -493,14 +542,17 @@ REWARD
RightSiZE
RiplleyHD
RiPTATORz
RiTALiX
RiVER
RMT
RoCKRioT
ROVERS
RSG
RTA
RUBY
RuDE
RUDOS
RUSTLE
Ryugan
S26
SAiMORNY
@ -508,10 +560,12 @@ SAiNTS
SAiVERT
SAMFD
SANTI
saphire
SAPHiRE
Sapphire
SChiZO
Scratch404
Scratched
ScWb
SecretMyth
SECTOR7
SEMTEX
@ -526,6 +580,7 @@ SHDXXX
shortbrehd
SHS
SHUNPO
SiC
SiGHTHD
SiHD
SiLU
@ -536,6 +591,7 @@ SLM
SLO
SMoKeR
Sneak
SNUGGLER
SoCkS
Softfeng
SoW
@ -543,6 +599,7 @@ SpaceHD
SPARKS
SPOOKY
SSF
Stealthmaster
stieg
Stranded
streetwars
@ -556,18 +613,24 @@ Swesub
SYS
t00ng0d
Taka
TARGET
TASTE
TASTETV
TB
TDF
TELEFLiX
TENEIGHTY
TERRA
terribleHD
terribleSD
THENiGHTMAREiNHD
TheWretched
THOR
THORA
THUGLiNE
TiDE
TiMELORDS
TiMPE
TiMTY
TiTANS
TjHD
@ -599,16 +662,21 @@ UNVEiL
USELESS
UVall
VaAr3
VALiOMEDiA
VAMPS
Vanillapunk
VanRay
VCDVaULT
VeGaN
Vegapunk
ViCiOsO
ViKAT
ViNYL
ViP3R
ViSiON
ViSTA
ViSTA™
ViTE
VLiS
VOA
VoMiT
@ -618,6 +686,7 @@ VoXHD
vrs
w0rm
w4f
WAF
WANKAZ
WASTE
WAVEY
@ -643,6 +712,7 @@ XSHD
XSTREEM
XTM
XTSF
XviK
XXX4U
YanY
YellowBeast