├── 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 | --------------------------------------------------------------------------------