├── resources
├── __init__.py
├── lib
│ ├── __init__.py
│ ├── id_drives.py
│ └── utils.py
├── language
│ ├── English
│ │ └── strings.xml
│ └── English (US)
│ │ └── strings.xml
└── settings.xml
├── icon.png
├── fanart.jpg
├── .gitignore
├── addon.xml
├── changelog.txt
├── LICENSE.txt
├── README.txt
└── default.py
/resources/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/lib/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reverendj1/kodi-couch-ripper/HEAD/icon.png
--------------------------------------------------------------------------------
/fanart.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reverendj1/kodi-couch-ripper/HEAD/fanart.jpg
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 |
5 | # C extensions
6 | *.so
7 |
8 | # Distribution / packaging
9 | .Python
10 | env/
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | lib64/
17 | parts/
18 | sdist/
19 | var/
20 | *.egg-info/
21 | .installed.cfg
22 | *.egg
23 |
24 | # PyInstaller
25 | # Usually these files are written by a python script from a template
26 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
27 | *.manifest
28 | *.spec
29 |
30 | # Installer logs
31 | pip-log.txt
32 | pip-delete-this-directory.txt
33 |
34 | # Unit test / coverage reports
35 | htmlcov/
36 | .tox/
37 | .coverage
38 | .cache
39 | nosetests.xml
40 | coverage.xml
41 |
42 | # Django stuff:
43 | *.log
44 |
45 | # Sphinx documentation
46 | docs/_build/
47 |
48 | # PyBuilder
49 | target/
50 |
--------------------------------------------------------------------------------
/addon.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | executable
8 |
9 |
10 | Rip DVDs and Blu Rays within Kodi
11 | Couch Ripper uses MakeMKV and Handbrake to rip movies into Kodi at the press of a button.
12 | Since Couch Ripper makes use of MakeMKV and Handbrake, those must be installed for it to work.
13 |
14 | all
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/changelog.txt:
--------------------------------------------------------------------------------
1 | 0.1.6:
2 | Bug Fixes:
3 | - Added workaround for Macs, or other systems using Python 2.6
4 | Features:
5 | - Added error logging when discs can't be read (usually due to being dirty
6 | or hardware issues)
7 |
8 | 0.1.5:
9 | Bug Fixes:
10 | - Shows error when writing to FAT32 drive
11 | - Fixed MakeMKV errors so they display the error in Kodi
12 | - Shows error when disc is missing
13 | - Fixed visible options, so they show correctly when using custom commands
14 | Features:
15 | - Added language chooser
16 |
17 | 0.1.4
18 | Bug Fixes:
19 | - Shows Error on Cancellation
20 | Misc:
21 | - Added note about temp dir
22 | - Added more debugging info
23 | - Added write access check for folders
24 |
25 | 0.1.3
26 | Features:
27 | - Added Custom Encode, Rip Commands
28 | 0.1.2
29 | - Version Bump
30 | 0.1.1
31 | Bug Fixes:
32 | - Fixed issue with CPU priority on Windows
33 | - Fixed error identifying drives
34 |
35 | 0.1.0
36 | - Initial Release
37 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2014 John Wesorick
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/resources/lib/id_drives.py:
--------------------------------------------------------------------------------
1 | import utils as utils
2 | import subprocess
3 | import urlparse
4 | import sys
5 |
6 |
7 | def main(argv):
8 |
9 | params = getParams(argv)
10 |
11 | if 'profile' in params:
12 | profilenum = params['profile']
13 |
14 | makemkvpath = utils.getSetting(profilenum + 'makemkvpath')
15 | if not makemkvpath:
16 | makemkvpath = utils.getSetting('defaultmakemkvpath')
17 | if not makemkvpath:
18 | # Please Set MakeMKVCon Path
19 | utils.exitFailed('{pleaseset} {pathtomakemkvcon}'.format(
20 | pleaseset = utils.getString(30072),
21 | pathtomakemkvcon = utils.getString(30016)),
22 | '{pleaseset} {pathtomakemkvcon}'.format(
23 | pleaseset = utils.getString(30072),
24 | pathtomakemkvcon = utils.getString(30016)))
25 | command = '"{makemkvpath}" info list -r'.format(makemkvpath=makemkvpath)
26 | try:
27 | if sys.version_info[:2] == (2,7):
28 | output = subprocess.check_output(
29 | command, stderr=subprocess.STDOUT, shell=True)
30 | elif sys.version_info[:2] == (2,6):
31 | output = utils.check_output(
32 | command, stderr=subprocess.STDOUT, shell=True)
33 | except subprocess.CalledProcessError, e:
34 | output = e.output
35 | gooddrives = ''
36 | # Iterate through the output, it should look like this:
37 | #MSG:1005,0,1,"MakeMKV v1.9.0 linux(x64-release) started","%1 started","MakeMKV v1.9.0 linux(x64-release)"
38 | #DRV:0,2,999,0,"BD-RE HL-DT-ST BD-RE WH14NS40 1.03","SquirrelWresting","/dev/sr0"
39 | #DRV:1,256,999,0,"","",""
40 | #DRV:2,256,999,0,"","",""
41 |
42 | lines = iter(output.splitlines())
43 | for line in lines:
44 | drive = line.split(',')
45 | if len(drive) >= 7:
46 | if drive[5] != '""':
47 | gooddrives = '{gooddrives} {drivenum}: {discname} '.format(
48 | gooddrives = gooddrives,
49 | drivenum = drive[0].split(':')[1],
50 | discname = drive[5].replace('"', ''))
51 |
52 | if gooddrives == '':
53 | # 30073 == Please Put a Disc in the Drive to Identify
54 | utils.exitFailed(utils.getString(30073), utils.getString(30073))
55 | else:
56 | utils.showOK(gooddrives)
57 | return 0
58 |
59 |
60 | def getParams(argv):
61 | param = {}
62 | if(len(argv) > 1):
63 | for i in argv:
64 | args = i
65 | if(args.startswith('?')):
66 | args = args[1:]
67 | param.update(dict(urlparse.parse_qsl(args)))
68 |
69 | return param
70 |
71 | if __name__ == '__main__':
72 | sys.exit(main(sys.argv))
73 |
--------------------------------------------------------------------------------
/resources/lib/utils.py:
--------------------------------------------------------------------------------
1 | import xbmc
2 | import xbmcgui
3 | import xbmcaddon
4 | import sys
5 | import subprocess
6 |
7 | __addon_id__ = 'script.couch_ripper'
8 | __Addon = xbmcaddon.Addon(__addon_id__)
9 |
10 |
11 | def data_dir():
12 | return __Addon.getAddonInfo('profile')
13 |
14 |
15 | def addon_dir():
16 | return __Addon.getAddonInfo('path')
17 |
18 |
19 | def openSettings():
20 | __Addon.openSettings()
21 |
22 |
23 | def log(message, loglevel=xbmc.LOGNOTICE):
24 | xbmc.log(encode('{couchripper}-{version} : {message}'.format(
25 | couchripper = __addon_id__,
26 | version = __Addon.getAddonInfo('version'),
27 | message= message)),
28 | level=loglevel)
29 |
30 | def check_output(*popenargs, **kwargs):
31 | r"""Run command with arguments and return its output as a byte string.
32 |
33 | Backported from Python 2.7 as it's implemented as pure python on stdlib.
34 |
35 | >>> check_output(['/usr/bin/python', '--version'])
36 | Python 2.6.2
37 | from https://gist.github.com/edufelipe/1027906
38 | """
39 | process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs)
40 | output, unused_err = process.communicate()
41 | retcode = process.poll()
42 | if retcode:
43 | cmd = kwargs.get("args")
44 | if cmd is None:
45 | cmd = popenargs[0]
46 | error = subprocess.CalledProcessError(retcode, cmd)
47 | error.output = output
48 | raise error
49 | return output
50 |
51 |
52 | def logDebug(message, loglevel=xbmc.LOGDEBUG):
53 | xbmc.log(encode('{couchripper}-{version} : {message}'.format(
54 | couchripper = __addon_id__,
55 | version = __Addon.getAddonInfo('version'),
56 | message= message)),
57 | level=loglevel)
58 |
59 |
60 | def showNotification(message):
61 | # 30010 == Couch Ripper
62 | xbmcgui.Dialog().notification(
63 | encode(getString(30010)),
64 | encode(message),
65 | time=4000,
66 | icon=xbmc.translatePath('{addonpath}/icon.png'.format(
67 | addonpath = __Addon.getAddonInfo('path'))))
68 |
69 |
70 | def showOK(message):
71 | return xbmcgui.Dialog().ok(
72 | encode(__Addon.getAddonInfo('name')),
73 | encode(message))
74 |
75 |
76 | def showSelectDialog(heading, selections):
77 | return xbmcgui.Dialog().select(encode(heading), selections)
78 |
79 |
80 | def settingsError(message):
81 | log('{message} {pleasecheckyoursettings}'.format(
82 | message = message,
83 | pleasecheckyoursettings = getString(30053)),
84 | xbmc.LOGERROR)
85 | return message
86 |
87 |
88 | def getSetting(name):
89 | return __Addon.getSetting(name)
90 |
91 |
92 | def getSettingLow(name):
93 | return __Addon.getSetting(name).lower()
94 |
95 |
96 | def setSetting(name, value):
97 | __Addon.setSetting(name, value)
98 |
99 |
100 | def getString(string_id):
101 | return __Addon.getLocalizedString(string_id)
102 |
103 |
104 | def getStringLow(string_id):
105 | return __Addon.getLocalizedString(string_id).lower()
106 |
107 |
108 | def exitFailed(message, error):
109 | log(encode(message), loglevel=xbmc.LOGERROR)
110 | log(encode(error), loglevel=xbmc.LOGERROR)
111 | # 30010 == Couch Ripper
112 | xbmcgui.Dialog().notification(
113 | encode(getString(30010)),
114 | encode(message),
115 | time=4000,
116 | icon=xbmcgui.NOTIFICATION_ERROR)
117 | sys.exit(1)
118 |
119 |
120 | def encode(string):
121 | result = ''
122 |
123 | try:
124 | result = string.encode('UTF-8', 'replace')
125 | except UnicodeDecodeError:
126 | result = 'Unicode Error'
127 |
128 | return result
129 |
--------------------------------------------------------------------------------
/resources/language/English/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Couch Ripper
4 | Resolution
5 | Quality
6 | High
7 | Medium
8 | Low
9 | Path to MakeMKVCon
10 | Path to HandBrakeCLI
11 | CPU Priority
12 | Normal
13 | Temporary Folder
14 | Destination Folder
15 | Minimum Title Length (minutes)
16 | Native Language (3 characters)
17 | Foreign Audio (grabs all audio streams)
18 | Encode After Rip
19 | Eject After
20 | Rip
21 | Encode
22 | Never
23 | Clean Up Temp Folder On Success
24 | Profile 1
25 | Profile 2
26 | Profile 3
27 | Profile 4
28 | Profile 5
29 | Profile 6
30 | Profile 7
31 | Profile 8
32 | Profile 9
33 | Profile 10
34 | Default
35 | 1080
36 | 720
37 | 480
38 | True
39 | False
40 | Pretty Name
41 | Enabled
42 | Notify After Rip
43 | Defaults
44 | Profile
45 | Could Not Find
46 | Please Check Your Settings.
47 | View Log for Details
48 | Cannot Write to
49 | Invalid
50 | There Were Errors Running
51 | Completed Successfully!
52 | Failed!
53 | Black and White
54 | Notify After Encode
55 | Show MakeMKVCon/Rip Command
56 | Show HandBrakeCLI/Encode Command
57 | Additional HandBrakeCLI Arguments
58 | Notification
59 | Dialog
60 | Disabled
61 | Drive ID
62 | Identify Drives (put disc in drive to ID)
63 | Beginning
64 | Command
65 | Please Set
66 | Please Put a Disc in the Drive to Identify
67 | Your temporary MakeMKV key has expired. Please update it.
68 | Your version of MakeMKV is too old. Please update it.
69 | Custom Rip Command
70 | Custom Encode Command
71 | Use Custom Rip Command
72 | Use Custom Encode Command
73 | There Are No Profiles Enabled!
74 | Rip Cancelled
75 | Could Not Write To
76 | Temp folder cannot handle large files.
77 | This is usually caused by using FAT32 for storage.
78 | Failed to Open Disc
79 | MakeMKV Had Trouble Reading the Disc. Try Cleaning it.
80 | MakeMKV Had Trouble Reading the Disc. Try Cleaning it if the Correct Title Wasn't Ripped.
81 | Update Library After Encode
82 |
83 |
--------------------------------------------------------------------------------
/resources/language/English (US)/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Couch Ripper
4 | Resolution
5 | Quality
6 | High
7 | Medium
8 | Low
9 | Path to MakeMKVCon
10 | Path to HandBrakeCLI
11 | CPU Priority
12 | Normal
13 | Temporary Folder
14 | Destination Folder
15 | Minimum Title Length (minutes)
16 | Native Language (3 characters)
17 | Foreign Audio (grabs all audio streams)
18 | Encode After Rip
19 | Eject After
20 | Rip
21 | Encode
22 | Never
23 | Clean Up Temp Folder On Success
24 | Profile 1
25 | Profile 2
26 | Profile 3
27 | Profile 4
28 | Profile 5
29 | Profile 6
30 | Profile 7
31 | Profile 8
32 | Profile 9
33 | Profile 10
34 | Default
35 | 1080
36 | 720
37 | 480
38 | True
39 | False
40 | Pretty Name
41 | Enabled
42 | Notify After Rip
43 | Defaults
44 | Profile
45 | Could Not Find
46 | Please Check Your Settings.
47 | View Log for Details
48 | Cannot Write to
49 | Invalid
50 | There Were Errors Running
51 | Completed Successfully!
52 | Failed!
53 | Black and White
54 | Notify After Encode
55 | Show MakeMKVCon/Rip Command
56 | Show HandBrakeCLI/Encode Command
57 | Additional HandBrakeCLI Arguments
58 | Notification
59 | Dialog
60 | Disabled
61 | Drive ID
62 | Identify Drives (put disc in drive to ID)
63 | Beginning
64 | Command
65 | Please Set
66 | Please Put a Disc in the Drive to Identify
67 | Your temporary MakeMKV key has expired. Please update it.
68 | Your version of MakeMKV is too old. Please update it.
69 | Custom Rip Command
70 | Custom Encode Command
71 | Use Custom Rip Command
72 | Use Custom Encode Command
73 | There Are No Profiles Enabled!
74 | Rip Cancelled
75 | Could Not Write To
76 | Temp folder cannot handle large files.
77 | This is usually caused by using FAT32 for storage.
78 | Failed to Open Disc
79 | MakeMKV Had Trouble Reading the Disc. Try Cleaning it.
80 | MakeMKV Had Trouble Reading the Disc. Try Cleaning it if the Correct Title Wasn't Ripped.
81 | Update Library After Encode
82 |
83 |
--------------------------------------------------------------------------------
/README.txt:
--------------------------------------------------------------------------------
1 | Couch Ripper Kodi Addon
2 |
3 | About:
4 | Like many others, I use Kodi and I like to rip my movies/TV shows to my
5 | library from DVDs and Blu-ray discs. I had been doing this with a few scripts
6 | and wanted to create a Kodi interface to make this a little smoother process.
7 | Couch Ripper is designed to do just that. This project is still very alpha.
8 |
9 | Pre-Requisites:
10 | By default, Couch Ripper utilizes MakeMKV and
11 | HandBrake to do the actual ripping and encoding of DVDs
12 | and Blu-rays, so these must be installed prior to it working, unless you
13 | specify different commands for encoding and ripping.
14 |
15 | MakeMKV:
16 | MakeMKV is the software responsible for ripping, decrypting and encoding the
17 | video. It is proprietary software, and costs $50 for a license, but is free to
18 | use while it is in beta. They have stated that once it leaves beta, DVD
19 | ripping will continue to be free, while Blu-ray ripping will require a
20 | license. During the beta, you can get the current license from
21 | which needs to be
22 | updated every 60 days. You can also show your support and purchase a full
23 | license (which will not change through versions or updates) for the software
24 | on their website .
25 |
26 | Installation - Ubuntu:
27 | sudo add-apt-repository ppa:heyarje/makemkv-beta
28 | sudo apt-get update
29 | sudo apt-get install makemkv-bin makemkv-oss
30 |
31 | Installation - Windows/Mac
32 | http://www.makemkv.com/download/
33 |
34 | Open the MakeMKV GUI and enter the key from the above-mentioned forum post, or
35 | your purchased license.
36 |
37 | HandBrake:
38 | HandBrake transcodes the files output by MakeMKV and compresses them to their
39 | final format. HandBrake is free and open-source software.
40 |
41 | Installation - Ubuntu (Do not use the version in the Ubuntu repos):
42 | sudo add-apt-repository ppa:stebbins/handbrake-releases
43 | sudo apt-get update
44 | sudo apt-get install handbrake-cli
45 |
46 | Installation - Windows/Mac
47 | https://handbrake.fr/downloads2.php
48 |
49 | Setup:
50 | I have tried to make Couch Ripper both easy to use and flexible. All of the
51 | defaults are pretty sane for a great quality movie with a small file size. The
52 | paths to the executables and folders have no defaults though, so those must be
53 | set prior to use. When you open the config, you will see a defaults section
54 | and 10 separate profiles. Set the defaults first, then you can use them in
55 | each profile. I like to use several different profiles, like Movies, TV Shows,
56 | Foreign Films, Black and White, etc. To use the default value for a setting in
57 | one of the profiles, either select default from the dropdown, or leave it
58 | blank. All profiles are set to use the values you set in the defaults section
59 | when they are created by default.
60 |
61 | Pretty Name................The name of the profile that will be shown in the
62 | selection dialog when you run Couch Ripper.
63 | Enabled....................Whether or not this profile should be shown in the
64 | selection dialog when you run Couch Ripper.
65 | Path to MakeMKVCon.........Required. Path to the makemkvcon executable
66 | (/usr/bin/makemkvcon on Ubuntu)
67 | Path to HandBrakeCLI.......Required. Path to the HandbrakeCLI executable
68 | (/usr/bin/HandBrakeCLI on Ubuntu)
69 | Temporary Folder...........Required. Path to temporary folder for initial
70 | rips. This folder will need at least 60GB free to
71 | store them. This folder should be dedicated to Couch
72 | Ripper, since anything in it can be deleted.
73 | Destination Folder.........Required. Path to where you want the final videos.
74 | I do not suggest making this the same path as your
75 | library, as the filenames will need cleanup.
76 | CPU Priority...............Ripping and encoding videos is very CPU intensive.
77 | Setting this to Normal or High may make the process
78 | faster, but will likely leave your PC unoperable in
79 | the meantime. Settings this to Low should allow you
80 | to use the regular functions of Kodi in the
81 | meantime.
82 | Resolution.................This is the maximum resolution. Videos with a
83 | higher resolution will be reduced to this. Videos
84 | with a lower resolution are unaffected.
85 | Quality....................Quality setting. High is recommended, but if
86 | quality is not a concern, and disk space is, choose
87 | another value.
88 | Minimum Title Length.......Since MakeMKV works by ripping all video tracks
89 | above a certain length, instead of picking the
90 | largest one, we need to set a minimum title length,
91 | otherwise it will rip all the previews, features,
92 | etc. A sane default for movies is 45, TV shows 8.
93 | Native Language............This is the language YOU speak. This setting makes
94 | sure that audio and subtitle tracks for your
95 | language are ripped, if available. In the case of
96 | foreign films where your language is not available,
97 | it will rip the language of the movie instead.
98 | Foreign Audio..............This setting is useful if you like to watch foreign
99 | movies with subtitles in their native language. It
100 | grabs all audio tracks available. If you do not
101 | like watching foreign films with subtitles, do not
102 | use this option. By setting the "Native Language"
103 | option above, your audio and subtitles will be
104 | ripped by default, if available.
105 | Encode After Rip...........As stated before, the process is split in two
106 | processes. First, a video is ripped, then it is
107 | encoded. Most users will want to leave this as
108 | True. The only reason to change it is if you wanted
109 | to encode your videos with another application, or
110 | perhaps batch encode them when your PC is not
111 | used, etc.
112 | Eject After................Choose when (if ever) to eject a disc.
113 | Notify After Rip...........Display a notification in Kodi after ripping a
114 | disc. Notification will be automatically dismissed.
115 | Dialog will pop up a notification that requires you
116 | to click OK to dismiss it. This is helpful if you
117 | won't be around to see the notification.
118 | Notify After Encode........Display a notification in Kodi after encoding a
119 | movie. Notification is a toast that will
120 | automatically be dismissed. Dialog will pop up a
121 | notification that requires you to click OK to
122 | dismiss it. This is helpful if you won't be around
123 | to see the notification.
124 | Clean Up Temp Folder On
125 | ..Success..................Delete temporary files after a successful encode.
126 | Most users would want to allow this, unless you are
127 | experiencing issues.
128 | Black and White............If a movie is black and white, this will tell the
129 | encoder not to produce colors, which can reduce
130 | green tinge or rainbow shimmering in black and
131 | white encodes. DO NOT use this for videos that are
132 | in black and white with partial color, such as
133 | Sin City.
134 | Drive ID...................Numerical drive ID. If you have more than one
135 | optical drive you may need to specify the correct
136 | drive here. You can use Identify Drives to get
137 | the IDs.
138 | Additional HandBrakeCLI
139 | ..Arguments................You can enter any additional arguments for
140 | HandBrakeCLI here. Useful for audio settings and
141 | such that are not handled by Couch Ripper. NOTE:
142 | Entering arguments here that conflict with the
143 | arguments set by Couch Ripper will probably
144 | end badly.
145 | Identify Drives............You can use this to find out the numerical ID for
146 | your optical drives. Just make sure to have a disc
147 | in the drive(s) you want to ID.
148 | Use Custom Rip Command.....Enables the use of a completely custom rip command.
149 | Custom Rip Command.........The rip command that will be used instead of
150 | MakeMKV. It is not verified in any way, and you are
151 | on your own if you choose to use this.
152 | Use Custom Encode Command..Enables the use of a completely custom encode
153 | command.
154 | Custom Encode Command......The encode command that will be used instead of
155 | HandBrakeCLI. It is not verified in any way, and you
156 | are on your own if you choose to use this.
157 | Show MakeMKVCon/Rip
158 | ..Command..................This just displays the MakeMKVCon (or custom rip)
159 | command that will be run. Useful when debugging or
160 | using advanced options.
161 | Show HandBrakeCLI/Encode
162 | ..Command..................This just displays the MakeMKVCon (or custom encode
163 | command that will be run. Useful when debugging,
164 | using advanced options, or if you want to schedule
165 | encodings.
166 |
167 |
168 | Usage:
169 | After setting up Couch Ripper and creating a profile or two, all you have to
170 | do is launch it. It will then bring up a dialog to choose the profile you want
171 | to use. After selecting a profile, it will rip and encode the video(s) for
172 | you. That's it!
173 |
174 | CD Part of icon derived from work by http://gentleface.com
175 | Couch part of icon derived from work by http://lethalnik-art.deviantart.com,
176 | http://minimalcustomizers.deviantart.com
177 |
178 | TODO: Create encoding scheduler
179 | Add Manual Naming Option
180 | Add Renaming Options
181 | If only title, remove txx in filename
182 | Use strings.po
183 |
--------------------------------------------------------------------------------
/default.py:
--------------------------------------------------------------------------------
1 | import urlparse
2 | import xbmc
3 | import os
4 | import sys
5 | import glob
6 | import resources.lib.utils as utils
7 | import platform
8 | import subprocess
9 | import re
10 |
11 |
12 | def main(argv):
13 |
14 | params = getParams(argv)
15 |
16 | defaultsettings = getDefaults()
17 |
18 | if 'profile' in params:
19 | profilenum = params['profile']
20 | else:
21 | profilenum = ''
22 |
23 | profiledict = getProfile(defaultsettings, profilenum)
24 |
25 | verifyProfile(profiledict)
26 |
27 | # Let's see if we just want to show the commands in a Kodi
28 | # notification. This is useful if you want to verify settings or
29 | # cron them up manually.
30 | if 'getcommand' in params:
31 | if params['getcommand'] == 'makemkvcon':
32 | utils.showOK(buildMakeMKVConCommand(profiledict))
33 | return 0
34 | elif params['getcommand'] == 'handbrakecli':
35 | utils.showOK(buildHandBrakeCLICommand(
36 | profiledict, profiledict['tempfolder']))
37 | return 0
38 |
39 | utils.logDebug(profiledict)
40 | command = buildMakeMKVConCommand(profiledict)
41 |
42 | # TODO (??) user might want to specify a disc name in case the disc name
43 | # is shortened or changed in a way that metadata libraries don't understand
44 | # what movie this is. many times though, the below will give you what you want
45 | discName = getDiscName(profiledict)
46 |
47 | # Beginning Rip. Command:
48 | utils.log('{beginning} {rip}. {commandstr}: {command}'.format(
49 | beginning = utils.getString(30070),
50 | rip = utils.getString(30027),
51 | commandstr = utils.getString(30071),
52 | command = command))
53 | try:
54 | if sys.version_info[:2] == (2,7):
55 | ripoutput = subprocess.check_output(
56 | command, stderr=subprocess.STDOUT, shell=True)
57 | elif sys.version_info[:2] == (2,6):
58 | ripoutput = utils.check_output(
59 | command, stderr=subprocess.STDOUT, shell=True)
60 | # For some reason, it seems that this always exits with a non-zero
61 | # status, so I'm just checking the output for success.
62 | except subprocess.CalledProcessError, e:
63 | if 'Copy complete.' in e.output:
64 | # We'll check for an error which denote using FAT32 for the temp
65 | # filesystem.
66 | fatcheck = re.search( r"The size of output file '(.*)' may reach "
67 | "as much as (.*) while target filesystem has a file size "
68 | "limit of (.*)", output)
69 | if fatcheck:
70 | # 30083 = Temp folder cannot handle large files
71 | # 30084 = This is usually caused by using FAT32 for storage.
72 | utils.exitFailed(utils.getstring(30083),
73 | utils.getString(30083) + ' ' + utils.getString(30084))
74 | if ('The source file' in e.output and
75 | ' is corrupt or invalid at offset' in e.output and
76 | ', attempting to work around' in e.output):
77 | # 30087 = MakeMKV Had Trouble Reading the Disc. Try Cleaning it
78 | # if the Correct Title Wasn't Ripped.
79 | utils.log(utils.getString(30087))
80 | else:
81 | if 'Your temporary key has expired and was removed' in e.output:
82 | # 30074 = Your temporary MakeMKV key has expired. Please update
83 | # it
84 | utils.exitFailed(utils.getString(30074),
85 | utils.getString(30074))
86 | if 'This application version is too old' in e.output:
87 | # 30075 = Your version of MakeMKV is too old. Please update it.
88 | utils.exitFailed(utils.getString(30075),
89 | utils.getString(30075))
90 | if 'Failed to open disc' in e.output:
91 | # 30085 = Failed to Open Disc
92 | utils.exitFailed(utils.getString(30085),
93 | utils.getString(30085))
94 | if ('The source file' in e.output and
95 | ' is corrupt or invalid at offset' in e.output and
96 | ', attempting to work around' in e.output):
97 | # 30086 = MakeMKV Had Trouble Reading the Disc. Try Cleaning it
98 | utils.exitFailed(utils.getString(30086),
99 | utils.getString(30086))
100 | utils.exitFailed('MakeMKV {failed}'.format(
101 | failed = utils.getString(30059)), e.output)
102 | utils.logDebug(ripoutput)
103 |
104 | # Eject if we need to
105 | # 30027 == Rip
106 | if profiledict['ejectafter'] == utils.getStringLow(30027):
107 | xbmc.executebuiltin('EjectTray()')
108 |
109 | # Display notification/dialog if we need to. A notification is a
110 | # toast that auto-dismisses after a few seconds, whereas a dialog
111 | # requires the user to press ok.
112 | # 30065 == Notification
113 | if profiledict['notifyafterrip'] == utils.getStringLow(30065):
114 | utils.showNotification('{rip} {completedsuccessfully}'.format(
115 | rip = utils.getString(30027),
116 | completedsuccessfully = utils.getString(30058)))
117 | # 30066 == Dialog
118 | elif profiledict['notifyafterrip'] == utils.getStringLow(30066):
119 | utils.showOK('{rip} {completedsuccessfully}'.format(
120 | rip = utils.getString(30027),
121 | completedsuccessfully = utils.getString(30058)))
122 |
123 | # Some people may want to just rip movies, and not encode them.
124 | # If that's the case, we are done here.
125 | if profiledict['encodeafterrip'] == 'false':
126 | return 0
127 |
128 | filestoencode = glob.glob(os.path.join(profiledict['tempfolder'], '*.mkv'))
129 | for f in filestoencode:
130 | # makemkvcon doesn't allow to customize the output filename
131 | # but handbrake does, so pass it in to customize output filename option
132 | command = buildHandBrakeCLICommand(profiledict, f, discName)
133 | utils.log('{beginning} {encode}. {commandstr}: {command}'.format(
134 | beginning = utils.getString(30070),
135 | encode = utils.getString(30028),
136 | commandstr = utils.getString(30071),
137 | command = command))
138 | try:
139 | if sys.version_info[:2] == (2,7):
140 | encodeoutput = subprocess.check_output(
141 | command, stderr=subprocess.STDOUT, shell=True)
142 | elif sys.version_info[:2] == (2,6):
143 | encodeoutput = utils.check_output(
144 | command, stderr=subprocess.STDOUT, shell=True)
145 | except subprocess.CalledProcessError, e:
146 | if 'Encode done!' not in e.output:
147 | utils.exitFailed('HandBrake {failed}'.format(
148 | failed = utils.getString(30059)), e.output)
149 | utils.logDebug(encodeoutput)
150 | if profiledict['cleanuptempdir'] == 'true':
151 | os.remove(f)
152 |
153 | # 30028 == Encode
154 | if profiledict['ejectafter'] == utils.getStringLow(30028):
155 | xbmc.executebuiltin('EjectTray()')
156 |
157 | # 30065 == Notification
158 | if profiledict['notifyafterencode'] == utils.getStringLow(30065):
159 | utils.showNotification('{encode} {completedsuccessfully}'.format(
160 | encode = utils.getString(30028),
161 | completedsuccessfully = utils.getString(30058)))
162 | elif profiledict['notifyafterencode'] == utils.getStringLow(30066):
163 | utils.showOK('{encode} {completedsuccessfully}'.format(
164 | encode = utils.getString(30028),
165 | completedsuccessfully = utils.getString(30058)))
166 |
167 | # if we have a name, the scan could pull in the metadata automatically for us
168 | if discName != None and profiledict['updatelibraryafterencode'] == 'true':
169 | xbmc.executebuiltin('UpdateLibrary("video")')
170 |
171 | return 0
172 |
173 |
174 | def getDefaults():
175 | # Get all the profile's settings. I know there has to be a better
176 | # way to do profiles, but this should work.
177 | defaultsettings = {
178 | 'defaultmakemkvpath': utils.getSetting('defaultmakemkvpath'),
179 | 'defaulthandbrakeclipath': utils.getSetting('defaulthandbrakeclipath'),
180 | 'defaulttempfolder': utils.getSetting('defaulttempfolder'),
181 | 'defaultdestinationfolder':
182 | utils.getSetting('defaultdestinationfolder'),
183 | 'defaultniceness': utils.getSettingLow('defaultniceness'),
184 | 'defaultresolution': utils.getSetting('defaultresolution'),
185 | 'defaultquality': utils.getSettingLow('defaultquality'),
186 | 'defaultmintitlelength': utils.getSetting('defaultmintitlelength'),
187 | 'defaultnativelanguage': utils.getSettingLow('defaultnativelanguage'),
188 | 'defaultforeignaudio': utils.getSettingLow('defaultforeignaudio'),
189 | 'defaultencodeafterrip': utils.getSettingLow('defaultencodeafterrip'),
190 | 'defaultupdatelibraryafterencode': utils.getSettingLow('defaultupdatelibraryafterencode'),
191 | 'defaultejectafter': utils.getSettingLow('defaultejectafter'),
192 | 'defaultnotifyafterrip': utils.getSettingLow('defaultnotifyafterrip'),
193 | 'defaultnotifyafterencode':
194 | utils.getSettingLow('defaultnotifyafterencode'),
195 | 'defaultcleanuptempdir': utils.getSettingLow('defaultcleanuptempdir'),
196 | 'defaultblackandwhite': utils.getSettingLow('defaultblackandwhite'),
197 | 'defaultdriveid': utils.getSetting('defaultdriveid'),
198 | 'defaultenablecustomripcommand':
199 | utils.getSetting('defaultenablecustomripcommand'),
200 | 'defaultcustomripcommand': utils.getSetting('defaultcustomripcommand'),
201 | 'defaultenablecustomencodecommand':
202 | utils.getSetting('defaultenablecustomencodecommand'),
203 | 'defaultcustomencodecommand':
204 | utils.getSetting('defaultcustomencodecommand'),
205 | 'defaultadditionalhandbrakeargs':
206 | utils.getSetting('defaultadditionalhandbrakeargs')}
207 | # Parse the 3 letter language code from selection
208 | languagesearch = re.search( r"(.*\()(.*)\)",
209 | defaultsettings['defaultnativelanguage'])
210 | if languagesearch:
211 | defaultsettings['defaultnativelanguage'] = languagesearch.group(2)
212 | return defaultsettings
213 |
214 |
215 | def getProfile(defaultsettings, profilenum):
216 | if profilenum == '':
217 | validprofiles = []
218 | for profile in ['profile1', 'profile2', 'profile3', 'profile4',
219 | 'profile5', 'profile6', 'profile7', 'profile8', 'profile9',
220 | 'profile10']:
221 | if utils.getSetting(profile + 'enabled') == 'true':
222 | validprofiles.append(utils.getSetting(profile + 'prettyname'))
223 | if validprofiles == []:
224 | utils.exitFailed(utils.getString(30080), utils.getString(30080))
225 | profilenum = utils.showSelectDialog(
226 | '{couchripper} - {profilestr}'.format(
227 | couchripper = utils.getString(30010),
228 | profilestr = utils.getString(30051)),
229 | validprofiles)
230 | profilename = validprofiles[profilenum]
231 | else:
232 | profilename = utils.getSetting(profilenum + 'prettyname')
233 | if profilenum == -1:
234 | # 30081 == Rip Cancelled
235 | utils.exitFailed(utils.getString(30081), utils.getString(30081))
236 | profiledict = []
237 | for profile in ['profile1', 'profile2', 'profile3', 'profile4', 'profile5',
238 | 'profile6', 'profile7', 'profile8', 'profile9', 'profile10']:
239 | if utils.getSetting(profile + 'prettyname') == profilename:
240 | profiledict = {
241 | 'makemkvpath':
242 | utils.getSetting(profile + 'makemkvpath'),
243 | 'handbrakeclipath':
244 | utils.getSetting(profile + 'handbrakeclipath'),
245 | 'tempfolder':
246 | utils.getSetting(profile + 'tempfolder'),
247 | 'destinationfolder':
248 | utils.getSetting(profile + 'destinationfolder'),
249 | 'niceness':
250 | utils.getSettingLow(profile + 'niceness'),
251 | 'resolution':
252 | utils.getSettingLow(profile + 'resolution'),
253 | 'quality':
254 | utils.getSettingLow(profile + 'quality'),
255 | 'mintitlelength':
256 | utils.getSettingLow(profile + 'mintitlelength'),
257 | 'nativelanguage':
258 | utils.getSettingLow(profile + 'nativelanguage'),
259 | 'foreignaudio':
260 | utils.getSettingLow(profile + 'foreignaudio'),
261 | 'encodeafterrip':
262 | utils.getSettingLow(profile + 'encodeafterrip'),
263 | 'updatelibraryafterencode':
264 | utils.getSettingLow(profile + 'updatelibraryafterencode'),
265 | 'ejectafter':
266 | utils.getSettingLow(profile + 'ejectafter'),
267 | 'notifyafterrip':
268 | utils.getSettingLow(profile + 'notifyafterrip'),
269 | 'notifyafterencode':
270 | utils.getSettingLow(profile + 'notifyafterencode'),
271 | 'cleanuptempdir':
272 | utils.getSettingLow(profile + 'cleanuptempdir'),
273 | 'blackandwhite':
274 | utils.getSettingLow(profile + 'blackandwhite'),
275 | 'driveid':
276 | utils.getSetting(profile + 'driveid'),
277 | 'enablecustomripcommand':
278 | utils.getSetting(profile + 'enablecustomripcommand'),
279 | 'customripcommand':
280 | utils.getSetting(profile + 'customripcommand'),
281 | 'enablecustomencodecommand':
282 | utils.getSetting(profile + 'enablecustomencodecommand'),
283 | 'customencodecommand':
284 | utils.getSetting(profile + 'customencodecommand'),
285 | 'additionalhandbrakeargs':
286 | utils.getSetting(profile + 'additionalhandbrakeargs')}
287 | # Parse the 3 letter language code from selection
288 | languagesearch = re.search( r"(.*\()(.*)\)",
289 | profiledict['nativelanguage'])
290 | if languagesearch:
291 | profiledict['nativelanguage'] = languagesearch.group(2)
292 | for key, value in profiledict.iteritems():
293 | if (value == 'default' or value == ''):
294 | profiledict[key] = defaultsettings['default' + key]
295 | return profiledict
296 |
297 |
298 | def verifyProfile(profiledict):
299 | # Let's verify all of our settings.
300 | errors = ''
301 | # 30013 == High, 30015 == Low, 30019 == Normal
302 | if (profiledict['niceness'] != utils.getStringLow(30013) and
303 | profiledict['niceness'] != utils.getStringLow(30015) and
304 | profiledict['niceness'] != utils.getStringLow(30019)):
305 | errors = errors + utils.settingsError(
306 | '{invalid} {niceness}. '.format(
307 | invalid = utils.getString(30056),
308 | niceness = utils.getString(30018)))
309 | if (profiledict['updatelibraryafterencode'] != 'true' and
310 | profiledict['updatelibraryafterencode'] != 'false'):
311 | errors = errors + utils.settingsError(
312 | '{invalid} {updatelibraryafterencode}. '.format(
313 | invalid = utils.getString(30056),
314 | updatelibraryafterencode = utils.getString(30025)))
315 | if (profiledict['encodeafterrip'] != 'true' and
316 | profiledict['encodeafterrip'] != 'false'):
317 | errors = errors + utils.settingsError(
318 | '{invalid} {encodeafterrip}. '.format(
319 | invalid = utils.getString(30056),
320 | encodeafterrip = utils.getString(30025)))
321 | # 30027 == Rip, 30028 == Encode, 30029 == Never
322 | if (profiledict['ejectafter'] != utils.getStringLow(30027) and
323 | profiledict['ejectafter'] != utils.getStringLow(30028) and
324 | profiledict['ejectafter'] != utils.getStringLow(30029)):
325 | errors = errors + utils.settingsError(
326 | '{invalid} {ejectafter}. '.format(
327 | invalid = utils.getString(30056),
328 | ejectafter = utils.getString(30026)))
329 | # 30065 == Notification,
330 | if (profiledict['notifyafterrip'] != utils.getStringLow(30065) and
331 | profiledict['notifyafterrip'] != utils.getStringLow(30066) and
332 | profiledict['notifyafterrip'] != utils.getStringLow(30067)):
333 | errors = errors + utils.settingsError(
334 | '{invalid} {notifyafterrip}. '.format(
335 | invalid = utils.getString(30056),
336 | notifyafterrip = utils.getString(30049)))
337 | if (profiledict['notifyafterencode'] != utils.getStringLow(30065) and
338 | profiledict['notifyafterencode'] != utils.getStringLow(30066) and
339 | profiledict['notifyafterencode'] != utils.getStringLow(30067)):
340 | errors = errors + utils.settingsError(
341 | '{invalid} {notifyafterencode}. '.format(
342 | invalid = utils.getString(30056),
343 | notifyafterencode = utils.getString(30061)))
344 | if (profiledict['enablecustomripcommand'] != 'true' and
345 | profiledict['enablecustomripcommand'] != 'false'):
346 | errors = errors + utils.settingsError(
347 | '{invalid} {enablecustomripcommand}. '.format(
348 | invalid = utils.getString(30056),
349 | enablecustomripcommand = utils.getString(30078)))
350 | if (profiledict['enablecustomencodecommand'] != 'true' and
351 | profiledict['enablecustomencodecommand'] != 'false'):
352 | errors = errors + utils.settingsError(
353 | '{invalid} {enablecustomencodecommand}. '.format(
354 | invalid = utils.getString(30056),
355 | enablecustomencodecommand = utils.getString(30079)))
356 |
357 | if (profiledict['enablecustomripcommand'] == 'false' and
358 | profiledict['enablecustomencodecommand'] == 'false'):
359 | if os.path.isdir(profiledict['tempfolder']):
360 | if not os.access(profiledict['tempfolder'], os.W_OK):
361 | errors = errors + utils.settingsError(
362 | '{couldnotwriteto} {tempfolder}. '.format(
363 | couldnotwriteto = utils.getString(30082),
364 | tempfolder = profiledict['tempfolder']))
365 | else:
366 | errors = errors + utils.settingsError(
367 | '{couldnotfind} {tempfolder}. '.format(
368 | couldnotfind = utils.getString(30052),
369 | tempfolder = profiledict['tempfolder']))
370 | if profiledict['enablecustomripcommand'] == 'false':
371 | if not os.path.isfile(profiledict['makemkvpath']):
372 | errors = errors + utils.settingsError(
373 | '{couldnotfind} makemkvcon. '.format(
374 | couldnotfind = utils.getString(30052)))
375 | if not profiledict['mintitlelength'].isdigit():
376 | errors = errors + utils.settingsError(
377 | '{invalid} {mintitlelength}. '.format(
378 | invalid = utils.getString(30056),
379 | mintitlelength = utils.getString(30022)))
380 | if not profiledict['driveid'].isdigit():
381 | errors = errors + utils.settingsError(
382 | '{invalid} {driveid}. '.format(
383 | invalid = utils.getString(30056),
384 | driveid = utils.getString(30068)))
385 | else:
386 | if profiledict['customripcommand'] == '':
387 | errors = errors + utils.settingsError(
388 | '{invalid} {customripcommand}. '.format(
389 | invalid = utils.getString(30056),
390 | customripcommand = utils.getString(30076)))
391 | if (profiledict['enablecustomencodecommand'] == 'true' and
392 | profiledict['customencodecommand'] == ''):
393 | errors = errors + utils.settingsError(
394 | '{invalid} {customencodecommand}. '.format(
395 | invalid = utils.getString(30056),
396 | customencodecommand = utils.getString(30077)))
397 | if (profiledict['enablecustomencodecommand'] == 'false' and
398 | profiledict['encodeafterrip'] == 'true'):
399 | if not os.path.isfile(profiledict['handbrakeclipath']):
400 | errors = errors + utils.settingsError(
401 | '{couldnotfind} HandBrakeCLI. '.format(
402 | couldnotfind = utils.getString(30052)))
403 | if os.path.isdir(profiledict['destinationfolder']):
404 | if not os.access(profiledict['destinationfolder'], os.W_OK):
405 | errors = errors + utils.settingsError(
406 | '{couldnotwriteto} {destinationfolder}. '.format(
407 | couldnotwriteto = utils.getString(30082),
408 | destinationfolder = profiledict['destinationfolder']))
409 | else:
410 | errors = errors + utils.settingsError(
411 | '{couldnotfind} {destinationfolder}. '.format(
412 | couldnotfind = utils.getString(30052),
413 | destinationfolder = profiledict['destinationfolder']))
414 | # 30042 == 1080, 30043 == 720, 30044 == 480
415 | if (profiledict['resolution'] != utils.getStringLow(30042) and
416 | profiledict['resolution'] != utils.getStringLow(30043) and
417 | profiledict['resolution'] != utils.getStringLow(30044)):
418 | errors = errors + utils.settingsError(
419 | '{invalid} {resolution}. '.format(
420 | invalid = utils.getString(30056),
421 | resolution = utils.getString(30011)))
422 | # 30013 == High, 30015 == Low, 30014 == Medium
423 | if (profiledict['quality'] != utils.getStringLow(30013) and
424 | profiledict['quality'] != utils.getStringLow(30014) and
425 | profiledict['quality'] != utils.getStringLow(30015)):
426 | errors = errors + utils.settingsError(
427 | '{invalid} {quality}. '.format(
428 | invalid = utils.getString(30056),
429 | quality = utils.getString(30012)))
430 | # List of valid ISO-639.2 language names.
431 | # From http://www.loc.gov/standards/iso639-2/ISO-639-2_8859-1.txt This
432 | # is the format that HandBrake requires language arguments to be in.
433 | validlanguages = [
434 | 'all', 'aar', 'abk', 'ace', 'ach', 'ada', 'ady', 'afa', 'afh',
435 | 'afr', 'ain', 'aka', 'akk', 'alb', 'ale', 'alg', 'alt', 'amh',
436 | 'ang', 'anp', 'apa', 'ara', 'arc', 'arg', 'arm', 'arn', 'arp',
437 | 'art', 'arw', 'asm', 'ast', 'ath', 'aus', 'ava', 'ave', 'awa',
438 | 'aym', 'aze', 'bad', 'bai', 'bak', 'bal', 'bam', 'ban', 'baq',
439 | 'bas', 'bat', 'bej', 'bel', 'bem', 'ben', 'ber', 'bho', 'bih',
440 | 'bik', 'bin', 'bis', 'bla', 'bnt', 'bos', 'bra', 'bre', 'btk',
441 | 'bua', 'bug', 'bul', 'bur', 'byn', 'cad', 'cai', 'car', 'cat',
442 | 'cau', 'ceb', 'cel', 'cha', 'chb', 'che', 'chg', 'chi', 'chk',
443 | 'chm', 'chn', 'cho', 'chp', 'chr', 'chu', 'chv', 'chy', 'cmc',
444 | 'cop', 'cor', 'cos', 'cpe', 'cpf', 'cpp', 'cre', 'crh', 'crp',
445 | 'csb', 'cus', 'cze', 'dak', 'dan', 'dar', 'day', 'del', 'den',
446 | 'dgr', 'din', 'div', 'doi', 'dra', 'dsb', 'dua', 'dum', 'dut',
447 | 'dyu', 'dzo', 'efi', 'egy', 'eka', 'elx', 'eng', 'enm', 'epo',
448 | 'est', 'ewe', 'ewo', 'fan', 'fao', 'fat', 'fij', 'fil', 'fin',
449 | 'fiu', 'fon', 'fre', 'frm', 'fro', 'frr', 'frs', 'fry', 'ful',
450 | 'fur', 'gaa', 'gay', 'gba', 'gem', 'geo', 'ger', 'gez', 'gil',
451 | 'gla', 'gle', 'glg', 'glv', 'gmh', 'goh', 'gon', 'gor', 'got',
452 | 'grb', 'grc', 'gre', 'grn', 'gsw', 'guj', 'gwi', 'hai', 'hat',
453 | 'hau', 'haw', 'heb', 'her', 'hil', 'him', 'hin', 'hit', 'hmn',
454 | 'hmo', 'hrv', 'hsb', 'hun', 'hup', 'iba', 'ibo', 'ice', 'ido',
455 | 'iii', 'ijo', 'iku', 'ile', 'ilo', 'ina', 'inc', 'ind', 'ine',
456 | 'inh', 'ipk', 'ira', 'iro', 'ita', 'jav', 'jbo', 'jpn', 'jpr',
457 | 'jrb', 'kaa', 'kab', 'kac', 'kal', 'kam', 'kan', 'kar', 'kas',
458 | 'kau', 'kaw', 'kaz', 'kbd', 'kha', 'khi', 'khm', 'kho', 'kik',
459 | 'kin', 'kir', 'kmb', 'kok', 'kom', 'kon', 'kor', 'kos', 'kpe',
460 | 'krc', 'krl', 'kro', 'kru', 'kua', 'kum', 'kur', 'kut', 'lad',
461 | 'lah', 'lam', 'lao', 'lat', 'lav', 'lez', 'lim', 'lin', 'lit',
462 | 'lol', 'loz', 'ltz', 'lua', 'lub', 'lug', 'lui', 'lun', 'luo',
463 | 'lus', 'mac', 'mad', 'mag', 'mah', 'mai', 'mak', 'mal', 'man',
464 | 'mao', 'map', 'mar', 'mas', 'may', 'mdf', 'mdr', 'men', 'mga',
465 | 'mic', 'min', 'mis', 'mkh', 'mlg', 'mlt', 'mnc', 'mni', 'mno',
466 | 'moh', 'mon', 'mos', 'mul', 'mun', 'mus', 'mwl', 'mwr', 'myn',
467 | 'myv', 'nah', 'nai', 'nap', 'nau', 'nav', 'nbl', 'nde', 'ndo',
468 | 'nds', 'nep', 'new', 'nia', 'nic', 'niu', 'nno', 'nob', 'nog',
469 | 'non', 'nor', 'nqo', 'nso', 'nub', 'nwc', 'nya', 'nym', 'nyn',
470 | 'nyo', 'nzi', 'oci', 'oji', 'ori', 'orm', 'osa', 'oss', 'ota',
471 | 'oto', 'paa', 'pag', 'pal', 'pam', 'pan', 'pap', 'pau', 'peo',
472 | 'per', 'phi', 'phn', 'pli', 'pol', 'pon', 'por', 'pra', 'pro',
473 | 'pus', 'qaa-qtz', 'que', 'raj', 'rap', 'rar', 'roa', 'roh',
474 | 'rom', 'rum', 'run', 'rup', 'rus', 'sad', 'sag', 'sah', 'sai',
475 | 'sal', 'sam', 'san', 'sas', 'sat', 'scn', 'sco', 'sel', 'sem',
476 | 'sga', 'sgn', 'shn', 'sid', 'sin', 'sio', 'sit', 'sla', 'slo',
477 | 'slv', 'sma', 'sme', 'smi', 'smj', 'smn', 'smo', 'sms', 'sna',
478 | 'snd', 'snk', 'sog', 'som', 'son', 'sot', 'spa', 'srd', 'srn',
479 | 'srp', 'srr', 'ssa', 'ssw', 'suk', 'sun', 'sus', 'sux', 'swa',
480 | 'swe', 'syc', 'syr', 'tah', 'tai', 'tam', 'tat', 'tel', 'tem',
481 | 'ter', 'tet', 'tgk', 'tgl', 'tha', 'tib', 'tig', 'tir', 'tiv',
482 | 'tkl', 'tlh', 'tli', 'tmh', 'tog', 'ton', 'tpi', 'tsi', 'tsn',
483 | 'tso', 'tuk', 'tum', 'tup', 'tur', 'tut', 'tvl', 'twi', 'tyv',
484 | 'udm', 'uga', 'uig', 'ukr', 'umb', 'und', 'urd', 'uzb', 'vai',
485 | 'ven', 'vie', 'vol', 'vot', 'wak', 'wal', 'war', 'was', 'wel',
486 | 'wen', 'wln', 'wol', 'xal', 'xho', 'yao', 'yap', 'yid', 'yor',
487 | 'ypk', 'zap', 'zbl', 'zen', 'zgh', 'zha', 'znd', 'zul', 'zun',
488 | 'zxx', 'zza']
489 |
490 | if profiledict['nativelanguage'] not in validlanguages:
491 | errors = errors + utils.settingsError(
492 | '{invalid} {nativelanguage}. '.format(
493 | invalid = utils.getString(30056),
494 | nativelanguage = utils.getString(30023)))
495 | if (profiledict['foreignaudio'] != 'true' and
496 | profiledict['foreignaudio'] != 'false'):
497 | errors = errors + utils.settingsError(
498 | '{invalid} {foreignaudio}. '.format(
499 | invalid = utils.getString(30056),
500 | foreignaudio = utils.getString(30024)))
501 | if (profiledict['cleanuptempdir'] != 'true' and
502 | profiledict['cleanuptempdir'] != 'false'):
503 | errors = errors + utils.settingsError(
504 | '{invalid} {cleanuptempdir}. '.format(
505 | invalid = utils.getString(30056),
506 | cleanuptempdir = utils.getString(30030)))
507 | if (profiledict['blackandwhite'] != 'true' and
508 | profiledict['blackandwhite'] != 'false'):
509 | errors = errors + utils.settingsError(
510 | '{invalid} {blackandwhite}. '.format(
511 | invalid = utils.getString(30056),
512 | blackandwhite = utils.getString(30060)))
513 |
514 | if errors:
515 | utils.exitFailed(errors, errors)
516 |
517 |
518 | def buildMakeMKVConCommand(profiledict):
519 | niceness = ''
520 | # 30013 == High, 30015 == Low
521 | if (profiledict['niceness'] == utils.getString(30013).lower() or
522 | profiledict['niceness'] == utils.getString(30015).lower()):
523 | if (platform.system() == 'Windows'):
524 | if (profiledict['niceness'] == utils.getStringLow(30013)):
525 | # If you pass quoted text to the Windows "start" command
526 | # as the first argument, it treats this as the name of the
527 | # program you want to run, not the command you want to run,
528 | # so I just add an empty set of double-quotes to prevent
529 | # issues.
530 | niceness = 'start /wait /b /high "" '
531 | else:
532 | niceness = 'start /wait /b /low "" '
533 | else:
534 | if (profiledict['niceness'] == utils.getStringLow(30013)):
535 | niceness = 'nice -n -19 '
536 | else:
537 | niceness = 'nice -n 19 '
538 | if profiledict['enablecustomripcommand'] == 'true':
539 | command = '{niceness} {customripcommand}'.format(niceness = niceness,
540 | customripcommand = profiledict['customripcommand'])
541 | else:
542 | mintitlelength = str(int(profiledict['mintitlelength']) * 60)
543 |
544 | command = ('{niceness} "{makemkvpath}" mkv --decrypt disc:{driveid} '
545 | 'all --minlength={mintitlelength} "{tempfolder}"'.format(
546 | niceness = niceness,
547 | makemkvpath = profiledict['makemkvpath'],
548 | driveid = profiledict['driveid'],
549 | mintitlelength = mintitlelength,
550 | # MakeMKV will add a forward slash at the end of a folder if it
551 | # has a backslash. This is bad for Windows, so we strip the
552 | # trailing slash.
553 | tempfolder = profiledict['tempfolder'].rstrip('\\')))
554 | return command
555 |
556 |
557 | def buildHandBrakeCLICommand(profiledict, f, discName):
558 | niceness = ''
559 | # 30013 == High, 30015 == Low, 30014 == Medium
560 | if (profiledict['niceness'] == utils.getString(30013).lower()
561 | or profiledict['niceness'] == utils.getString(30015).lower()):
562 | if (platform.system() == 'Windows'):
563 | if (profiledict['niceness'] == utils.getStringLow(30013)):
564 | # If you pass quoted text to the Windows "start" command
565 | # as the first argument, it treats this as the name of the
566 | # program you want to run, not the command you want to run,
567 | # so I just add an empty set of double-quotes to prevent
568 | # issues.
569 | niceness = 'start /wait /b /high "" '
570 | else:
571 | niceness = 'start /wait /b /low "" '
572 | else:
573 | if (profiledict['niceness'] == utils.getStringLow(30013)):
574 | niceness = 'nice -n -19 '
575 | else:
576 | niceness = 'nice -n 19 '
577 | if profiledict['enablecustomencodecommand'] == 'true':
578 | command = '{niceness} {customencodecommand}'.format(niceness = niceness,
579 | customencodecommand = profiledict['customencodecommand'])
580 | else:
581 | if profiledict['resolution'] == '1080':
582 | maxwidth = ' --maxWidth 1920'
583 | elif profiledict['resolution'] == '720':
584 | maxwidth = ' --maxWidth 1280'
585 | elif profiledict['resolution'] == '480':
586 | maxwidth = ' --maxWidth 720'
587 |
588 | if profiledict['quality'] == utils.getStringLow(30013):
589 | quality = ''
590 | elif profiledict['quality'] == utils.getStringLow(30014):
591 | quality = ' -q 25 '
592 | elif profiledict['quality'] == utils.getStringLow(30015):
593 | quality = ' -q 26 '
594 |
595 | if profiledict['blackandwhite'] == 'true':
596 | blackandwhite = ' --grayscale '
597 | else:
598 | blackandwhite = ''
599 |
600 | additionalhandbrakeargs = ' {additionalhandbrakeargs}'.format(
601 | additionalhandbrakeargs =
602 | profiledict['additionalhandbrakeargs'])
603 |
604 | # To make things easier when we rip foreign films, we just grab all
605 | # the audio tracks. Otherwise it can be hard to grab just the
606 | # native language of the movie and the native language of the user.
607 | # Since grabbing all audio tracks doesn't appear to be an option
608 | # with HandBrake, I just grab the first ten tracks. When you list
609 | # audio tracks here that don't exist, it doesn't seem to error out
610 | # or anything.
611 | if profiledict['foreignaudio'] == 'true':
612 | audiotracks = ' -a 1,2,3,4,5,6,7,8,9,10 '
613 | else:
614 | audiotracks = ''
615 |
616 | if discName != None:
617 | if re.search(r"_t00?\.mkv$", os.path.basename(f)) != None or os.path.basename(f) in ['title00.mkv', 'title.mkv', 'title0.mkv']:
618 | destination = os.path.join(profiledict['destinationfolder'], os.path.basename(discName).replace('_', ' ') + os.path.splitext(f)[1])
619 | else:
620 | destination = os.path.join(profiledict['destinationfolder'], os.path.basename(discName + '-' + f).replace('_', ' '))
621 | else:
622 | destination = os.path.join(profiledict['destinationfolder'], os.path.basename(f).replace('_', ' '))
623 |
624 | command = ('{niceness}"{handbrakeclipath}" -i "{filename}" -o '
625 | '"{destination}" -f mkv -d slower -N {nativelanguage} '
626 | '--native-dub -m -Z "H.264 MKV 1080p30" -s 1{audiotracks}{quality}'
627 | '{blackandwhite}{maxwidth}{additionalhandbrakeargs}'.format(
628 | niceness = niceness,
629 | handbrakeclipath = profiledict['handbrakeclipath'],
630 | filename = f,
631 | destination = destination,
632 | nativelanguage = profiledict['nativelanguage'],
633 | audiotracks = audiotracks,
634 | quality = quality,
635 | blackandwhite = blackandwhite,
636 | maxwidth = maxwidth,
637 | additionalhandbrakeargs = additionalhandbrakeargs))
638 |
639 | return command
640 |
641 | def getDiscName(profiledict):
642 | makemkvpath = profiledict['makemkvpath']
643 | drive_id = int(profiledict['driveid'])
644 | command = '"{makemkvpath}" info list -r'.format(makemkvpath=makemkvpath)
645 | try:
646 | if sys.version_info[:2] == (2,7):
647 | output = subprocess.check_output(
648 | command, stderr=subprocess.STDOUT, shell=True)
649 | elif sys.version_info[:2] == (2,6):
650 | output = utils.check_output(
651 | command, stderr=subprocess.STDOUT, shell=True)
652 | except subprocess.CalledProcessError, e:
653 | output = e.output
654 | gooddrives = ''
655 | # Iterate through the output, it should look like this:
656 | #MSG:1005,0,1,"MakeMKV v1.9.0 linux(x64-release) started","%1 started","MakeMKV v1.9.0 linux(x64-release)"
657 | #DRV:0,2,999,0,"BD-RE HL-DT-ST BD-RE WH14NS40 1.03","SquirrelWresting","/dev/sr0"
658 | #DRV:1,256,999,0,"","",""
659 | #DRV:2,256,999,0,"","",""
660 |
661 | lines = iter(output.splitlines())
662 | for line in lines:
663 | drive = line.split(',')
664 | if len(drive) >= 7:
665 | if drive[5] != '""' and int(drive[0].split(':')[1]) == drive_id:
666 | return drive[5].replace('"', '')
667 |
668 | return None
669 |
670 | def getParams(argv):
671 | param = {}
672 | if(len(argv) > 1):
673 | for i in argv:
674 | args = i
675 | if(args.startswith('?')):
676 | args = args[1:]
677 | param.update(dict(urlparse.parse_qsl(args)))
678 |
679 | return param
680 |
681 | if __name__ == '__main__':
682 | sys.exit(main(sys.argv))
683 |
--------------------------------------------------------------------------------
/resources/settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
--------------------------------------------------------------------------------