You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

214 lines
7.7 KiB

# -*- coding: utf-8 -*-
#
# Subliminal - Subtitles, faster than your thoughts
# Copyright (c) 2011 Antoine Bertin <diaoulael@gmail.com>
#
# This file is part of Subliminal.
#
# Subliminal is free software; you can redistribute it and/or modify it under
# the terms of the Lesser GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Subliminal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
__all__ = ['EXTENSIONS', 'MIMETYPES', 'Video', 'Episode', 'Movie', 'UnknownVideo', 'scan']
from languages import list_languages
import abc
import enzyme
import guessit
import hashlib
import mimetypes
import os
import struct
import subprocess
import subtitles
EXTENSIONS = ['.avi', '.mkv', '.mpg', '.mp4', '.m4v', '.mov', '.ogm', '.ogv', '.wmv', '.divx', '.asf']
MIMETYPES = ['video/mpeg', 'video/mp4', 'video/quicktime', 'video/x-ms-wmv', 'video/x-msvideo', 'video/x-flv', 'video/x-matroska', 'video/x-matroska-3d']
class Video(object):
__metaclass__ = abc.ABCMeta
"""Base class for videos"""
def __init__(self, release, guess, imdbid=None):
self.release = release
self.guess = guess
self.imdbid = imdbid
self._path = None
self.hashes = {}
if os.path.exists(release):
self.path = release
@classmethod
def fromPath(cls, path):
"""Create a Video object guessing all informations from the given release/path"""
guess = guessit.guess_file_info(path, 'autodetect')
result = None
if guess['type'] == 'episode' and 'series' in guess and 'season' in guess and 'episodeNumber' in guess:
title = None
if 'title' in guess:
title = guess['title']
result = Episode(path, guess['series'], guess['season'], guess['episodeNumber'], title, guess)
if guess['type'] == 'movie' and 'title' in guess:
year = None
if 'year' in guess:
year = guess['year']
result = Movie(path, guess['title'], year, guess)
if not result:
result = UnknownVideo(path, guess)
if not isinstance(result, cls):
raise ValueError('Video is not of requested type')
return result
@property
def exists(self):
if self._path:
return os.path.exists(self._path)
return False
@property
def path(self):
return self._path
@path.setter
def path(self, value):
if not os.path.exists(value):
raise ValueError('Path does not exists')
self._path = value
self.size = os.path.getsize(self._path)
self._computeHashes()
def _computeHashes(self):
self.hashes['OpenSubtitles'] = self._computeHashOpenSubtitles()
self.hashes['TheSubDB'] = self._computeHashTheSubDB()
def _computeHashOpenSubtitles(self):
"""Hash a file like OpenSubtitles"""
longlongformat = 'q' # long long
bytesize = struct.calcsize(longlongformat)
f = open(self.path, 'rb')
filesize = os.path.getsize(self.path)
filehash = filesize
if filesize < 65536 * 2:
return []
for _ in range(65536 / bytesize):
filebuffer = f.read(bytesize)
(l_value,) = struct.unpack(longlongformat, filebuffer)
filehash += l_value
filehash = filehash & 0xFFFFFFFFFFFFFFFF # to remain as 64bit number
f.seek(max(0, filesize - 65536), 0)
for _ in range(65536 / bytesize):
filebuffer = f.read(bytesize)
(l_value,) = struct.unpack(longlongformat, filebuffer)
filehash += l_value
filehash = filehash & 0xFFFFFFFFFFFFFFFF
f.close()
returnedhash = '%016x' % filehash
return returnedhash
def _computeHashTheSubDB(self):
"""Hash a file like TheSubDB"""
readsize = 64 * 1024
with open(self.path, 'rb') as f:
data = f.read(readsize)
f.seek(-readsize, os.SEEK_END)
data += f.read(readsize)
return hashlib.md5(data).hexdigest()
def mkvmerge(self, subs, out=None, mkvmerge_bin='mkvmerge', title=None):
"""Merge the video with subs"""
if not out:
out = self.path + '.merged.mkv'
args = [mkvmerge_bin, '-o', out, self.path]
if title:
args += ['--title', title]
for sub in subs:
if sub.language:
track_id = 0
if isinstance(sub, subtitles.EmbeddedSubtitle):
track_id = sub.track_id
args += ['--language', str(track_id) + ':' + sub.language, sub.path]
continue
args += [sub.path]
with open(os.devnull, 'w') as devnull:
p = subprocess.Popen(args, stdout=devnull, stderr=devnull)
p.wait()
def scan(self):
"""Scan and return associated Subtitles"""
if not self.exists:
return []
basepath = os.path.splitext(self.path)[0]
results = []
video_infos = None
try:
video_infos = enzyme.parse(self.path)
except enzyme.ParseError:
pass
if isinstance(video_infos, enzyme.core.AVContainer):
results.extend([subtitles.EmbeddedSubtitle.fromEnzyme(self.path, s) for s in video_infos.subtitles])
for l in list_languages(1):
for e in subtitles.EXTENSIONS:
single_path = basepath + '%s' % e
if os.path.exists(single_path):
results.append(subtitles.ExternalSubtitle(single_path, None))
multi_path = basepath + '.%s%s' % (l, e)
if os.path.exists(multi_path):
results.append(subtitles.ExternalSubtitle(multi_path, l))
return results
class Episode(Video):
"""Episode class"""
def __init__(self, release, series, season, episode, title=None, guess=None, tvdbid=None, imdbid=None):
super(Episode, self).__init__(release, guess, imdbid)
self.series = series
self.title = title
self.season = season
self.episode = episode
self.tvdbid = tvdbid
class Movie(Video):
"""Movie class"""
def __init__(self, release, title, year=None, guess=None, imdbid=None):
super(Movie, self).__init__(release, guess, imdbid)
self.title = title
self.year = year
class UnknownVideo(Video):
"""Unknown video"""
def __init__(self, release, guess, imdbid=None):
super(UnknownVideo, self).__init__(release, guess, imdbid)
self.guess = guess
def scan(entry, max_depth=3, depth=0):
"""Scan a path and return a list of tuples (video, [subtitle])"""
if depth > max_depth and max_depth != 0: # we do not want to search the whole file system except if max_depth = 0
return []
if depth == 0:
entry = os.path.abspath(entry)
if os.path.isfile(entry): # a file? scan it
if depth != 0: # trust the user: only check for valid format if recursing
if mimetypes.guess_type(entry)[0] not in MIMETYPES and os.path.splitext(entry)[1] not in EXTENSIONS:
return []
video = Video.fromPath(entry)
return [(video, video.scan())]
if os.path.isdir(entry): # a dir? recurse
result = []
for e in os.listdir(entry):
result.extend(scan(os.path.join(entry, e), max_depth, depth + 1))
return result
return [] # anything else