split movie guesser into video+movie guesser
[smewt:guessit.git] / guessit / video.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # GuessIt - A library for guessing information from filenames
5 # Copyright (c) 2011 Nicolas Wack <wackou@gmail.com>
6 #
7 # GuessIt is free software; you can redistribute it and/or modify it under
8 # the terms of the Lesser GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # GuessIt is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # Lesser GNU General Public License for more details.
16 #
17 # You should have received a copy of the Lesser GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20
21 from guessit.guess import Guess, merge_all
22 from guessit import fileutils, textutils
23 import os.path
24 import re
25 import logging
26
27 log = logging.getLogger('guessit.video')
28
29
30 def format_video_guess(guess):
31     """Formats all the found values to their natural type.
32     For instance, a year would be stored as an int value, ...
33     Note that this modifies the dictionary given as input."""
34     for prop in [ 'year' ]:
35         try:
36             guess[prop] = int(guess[prop])
37         except KeyError:
38             pass
39
40     return guess
41
42
43 def valid_year(year):
44     try:
45         return int(year) > 1920 and int(year) < 2015
46     except ValueError:
47         return False
48
49
50
51
52 '''
53 def VideoFilename(filename):
54     parts = textutils.cleanString(filename).split()
55
56     found = {} # dictionary of identified named properties to their index in the parts list
57
58     # heuristic 1: find VO, sub FR, etc...
59     for i, part in enumerate(parts):
60         if matchRegexp(part, [ 'VO', 'VF' ]):
61             found = { ('audio', 'VO'): i }
62
63     # heuristic 2: match video size
64     #rexp('...x...') with x > 200  # eg: (720, 480) -> property format = 16/9, etc...
65
66     # we consider the name to be what's left at the beginning, before any other identified part
67     # (other possibility: look at the remaining zones in parts which are not covered by any identified props, look for the first one, or the biggest one)
68     name = ' '.join(parts[:min(found.values())])
69 '''
70
71 def guess_video_filename_parts(filename):
72     filename = os.path.basename(filename)
73     result = []
74
75     def guessed(match, confidence = None):
76         result.append(format_video_guess(Guess(match, confidence = confidence)))
77
78
79     # DVDRip.Xvid-$(grpname)
80     grpnames = [ '\.Xvid-(?P<releaseGroup>.*?)\.',
81                  '\.DivX-(?P<releaseGroup>.*?)\.'
82                  ]
83     editions = [ '(?P<edition>(special|unrated|criterion).edition)'
84                  ]
85     audio = [ '(?P<audioChannels>5\.1)' ]
86
87     specific = grpnames + editions + audio
88     for match in textutils.matchAllRegexp(filename, specific):
89         log.debug('Found with confidence 1.0: %s' % match)
90         guessed(match, confidence = 1.0)
91         for key, value in match.items():
92             filename = filename.replace(value, '')
93
94
95     # remove punctuation for looser matching now
96     seps = [ ' ', '-', '.', '_' ]
97     for sep in seps:
98         filename = filename.replace(sep, ' ')
99
100     # TODO: replace this with a getMetadataGroups function that splits on parentheses/braces/brackets
101     remove = [ '[', ']', '(', ')', '{', '}' ]
102     for rem in remove:
103         filename = filename.replace(rem, ' ')
104
105     name = filename.split(' ')
106
107
108     properties = { 'format': [ 'DVDRip', 'HDDVD', 'HDDVDRip', 'BDRip', 'R5', 'HDRip', 'DVD', 'Rip' ],
109                    'container': [ 'avi', 'mkv', 'ogv', 'wmv', 'mp4', 'mov' ],
110                    'screenSize': [ '720p' ],
111                    'videoCodec': [ 'XviD', 'DivX', 'x264', 'Rv10' ],
112                    'audioCodec': [ 'AC3', 'DTS', 'He-AAC', 'AAC-He', 'AAC' ],
113                    'language': [ 'english', 'eng',
114                                  'spanish', 'esp',
115                                  'french', 'fr',
116                                  'italian', # no 'it', too common a word in english
117                                  'vo', 'vf'
118                                  ],
119                    'releaseGroup': [ 'ESiR', 'WAF', 'SEPTiC', '[XCT]', 'iNT', 'PUKKA', 'CHD', 'ViTE', 'DiAMOND', 'TLF',
120                                      'DEiTY', 'FLAiTE', 'MDX', 'GM4F', 'DVL', 'SVD', 'iLUMiNADOS', ' FiNaLe', 'UnSeeN' ],
121                    'other': [ '5ch', 'PROPER', 'REPACK', 'LIMITED', 'DualAudio', 'iNTERNAL', 'Audiofixed',
122                               'classic', # not so sure about this one, could appear in a title
123                               'ws', # widescreen
124                               'SE', # special edition
125                               # TODO: director's cut
126                               ],
127                    }
128
129     # ensure they're all lowercase
130     for prop, value in properties.items():
131         properties[prop] = [ s.lower() for s in value ]
132
133
134     # to try to guess what part of the filename is the movie title, we only keep as
135     # possible title the first characters of the filename up to the leftmost metadata
136     # element we found, no more
137     minIdx = len(name)
138
139     # get specific properties
140     for prop, value in properties.items():
141         for part in name:
142             if part.lower() in value:
143                 log.debug('Found with confidence 1.0: %s' % { prop: part })
144                 guessed({ prop: part }, confidence = 1.0)
145                 minIdx = min(minIdx, name.index(part))
146
147
148     # get year
149     for part in name:
150         year = textutils.stripBrackets(part)
151         if valid_year(year):
152             year = int(year)
153             log.debug('Found with confidence 0.9: %s' % { 'year': year })
154             guessed({ 'year': year }, confidence = 0.9)
155             minIdx = min(minIdx, name.index(part))
156
157     # remove ripper name
158     # FIXME: fails with movies such as "down by law", etc...
159     for by, who in zip(name[:-1], name[1:]):
160         if by.lower() == 'by':
161             log.debug('Found with confidence 0.4: %s' % { 'ripper': who })
162             guessed({ 'ripper': who }, confidence = 0.4)
163             minIdx = min(minIdx, name.index(by))
164
165     # subtitles
166     # TODO: only finds the first one, need to check whether there are many of them
167     for sub, lang in zip(name[:-1], name[1:]):
168         if sub.lower() == 'sub':
169             log.debug('Found with confidence 0.7: %s' % { 'subtitleLanguage': lang })
170             guessed({ 'subtitleLanguage': lang }, confidence = 0.7)
171             minIdx = min(minIdx, name.index(sub))
172
173     # get CD number (if any)
174     cdrexp = re.compile('[Cc][Dd]([0-9]+)')
175     for part in name:
176         try:
177             cdnumber = int(cdrexp.search(part).groups()[0])
178             log.debug('Found with confidence 1.0: %s' % { 'cdnumber': cdnumber })
179             guessed({ 'cdnumber': cdnumber }, confidence = 1.0)
180             minIdx = min(minIdx, name.index(part))
181         except AttributeError:
182             pass
183
184     name = ' '.join(name[:minIdx])
185     minIdx = len(name)
186
187     # TODO: generic website url guesser
188     websites = [ 'sharethefiles.com', 'tvu.org.ru' ]
189     websites = [ '(?P<website>%s)' % w.replace('.', ' ') for w in websites ] # dots have been previously converted to spaces
190     # TODO: maybe we can add more patterns here?
191     rexps = websites
192
193     matched = textutils.matchAllRegexp(name, rexps)
194     for match in matched:
195         log.debug('Found with confidence 1.0: %s' % match)
196         guessed(match, confidence = 1.0)
197         for key, value in match.items():
198             minIdx = min(minIdx, name.find(value))
199
200
201     return result, minIdx
202
203
204 def guess_video_filename(filename):
205     """Return the guessed information about the video file, as well the idx in
206     the filename string starting with which we found something."""
207     parts, idx = guess_video_filename_parts(filename)
208
209     return merge_all(parts), idx