├── .editorconfig ├── .eslintignore ├── .gitattributes ├── .gitignore ├── .travis.yml ├── .yo-rc.json ├── LICENSE ├── README.md ├── __tests__ ├── app.js └── validationHelper.js ├── generators └── app │ ├── index.js │ ├── templates │ ├── README.md │ ├── addon.xml │ ├── changelog.txt │ ├── gitignore │ ├── main.py │ ├── resources │ │ ├── __init__.py │ │ ├── language │ │ │ ├── README.md │ │ │ └── resource.language.en_gb │ │ │ │ └── strings.po │ │ ├── lib │ │ │ ├── README.md │ │ │ ├── __init__.py │ │ │ ├── context.py │ │ │ ├── kodilogging.py │ │ │ ├── kodiutils.py │ │ │ ├── plugin.py │ │ │ ├── script.py │ │ │ ├── service.py │ │ │ └── subtitle.py │ │ └── settings.xml │ ├── tests │ │ └── README.md │ └── travis.yml │ └── validationHelper.js ├── package-lock.json ├── package.json ├── pictures └── example-script.gif └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | **/templates 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 12 5 | - '10' 6 | - '8' 7 | 8 | before_script: 9 | - 'git config --global user.email "you@example.com"' 10 | - 'git config --global user.name "Your Name"' 11 | 12 | after_script: 'cat ./coverage/lcov.info | coveralls' -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-node": { 3 | "promptValues": { 4 | "authorName": "Razzeee", 5 | "authorEmail": "razze@kodi.tv", 6 | "authorUrl": "" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Kolja Lampe 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # generator-kodi-addon [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][daviddm-image]][daviddm-url] [![Development Dependency Status][daviddm-image-dev]][daviddm-url-dev] [![Coverage percentage][coveralls-image]][coveralls-url] 2 | > Creates the basic structure for a kodi script, written in python. 3 | 4 | ![Example](pictures/example-script.gif) 5 | 6 | ## Installation 7 | 8 | First, install [Yeoman](http://yeoman.io) and generator-kodi-addon using [npm](https://www.npmjs.com/) (we assume you have pre-installed [node.js](https://nodejs.org/)). 9 | 10 | ```bash 11 | npm install -g yo 12 | npm install -g generator-kodi-addon 13 | ``` 14 | 15 | Then generate your new project: 16 | 17 | ```bash 18 | yo kodi-addon 19 | ``` 20 | 21 | ## Development 22 | 23 | - Check out this project via git 24 | - Browse to the folder via commandline 25 | - Do `npm link` 26 | - Now you can use `yo kodi-addon`, which should include your changes. 27 | 28 | ## License 29 | 30 | Apache-2.0 © [Kolja Lampe]() 31 | 32 | 33 | [npm-image]: https://badge.fury.io/js/generator-kodi-addon.svg 34 | [npm-url]: https://npmjs.org/package/generator-kodi-addon 35 | [travis-image]: https://travis-ci.org/xbmc/generator-kodi-addon.svg?branch=master 36 | [travis-url]: https://travis-ci.org/xbmc/generator-kodi-addon 37 | [daviddm-image]: https://david-dm.org/xbmc/generator-kodi-addon.svg?theme=shields.io 38 | [daviddm-url]: https://david-dm.org/xbmc/generator-kodi-addon 39 | [daviddm-image-dev]: https://david-dm.org/xbmc/generator-kodi-addon/dev-status.svg 40 | [daviddm-url-dev]: https://david-dm.org/xbmc/generator-kodi-addon?type=dev 41 | [coveralls-image]: https://coveralls.io/repos/xbmc/generator-kodi-addon/badge.svg 42 | [coveralls-url]: https://coveralls.io/r/xbmc/generator-kodi-addon 43 | -------------------------------------------------------------------------------- /__tests__/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | var assert = require('yeoman-assert'); 4 | var helpers = require('yeoman-test'); 5 | 6 | describe('generate contextmenu', function () { 7 | beforeEach(function () { 8 | return helpers.run(path.join(__dirname, '../generators/app')) 9 | .withPrompts({ 10 | type: 'Contextmenu', 11 | scriptid: 'contextmenu.test', 12 | scriptname: 'My contextmenu name', 13 | kodiVersion: '2.25.0', 14 | platforms: 'all', 15 | license: 'MIT', 16 | authors: 'Me', 17 | summary: 'My summary', 18 | authorName: 'My real name', 19 | email: 'test@test.de', 20 | website: 'www.kodi.tv' 21 | }) 22 | .toPromise(); 23 | }); 24 | 25 | it('creates contextmenu files', function () { 26 | assert.file([ 27 | 'addon.xml', 28 | '.gitignore', 29 | '.travis.yml', 30 | 'changelog.txt', 31 | 'main.py', 32 | 'README.md', 33 | 'resources/lib/context.py', 34 | 'resources/language/resource.language.en_gb/strings.po', 35 | 'resources/language/README.md', 36 | 'LICENSE' 37 | ]); 38 | assert.noFile([ 39 | 'resources/lib/plugin.py', 40 | 'resources/lib/service.py', 41 | 'resources/lib/script.py', 42 | 'resources/lib/subtitle.py', 43 | 'tests/README.md', 44 | 'resources/__init__.py', 45 | 'resources/lib/__init__.py', 46 | 'resources/lib/kodiutils.py', 47 | 'resources/lib/kodilogging.py', 48 | 'resources/lib/README.md', 49 | 'resources/settings.xml' 50 | ]); 51 | }); 52 | it('check contextmenu addon.xml content', function () { 53 | assert.fileContent('addon.xml', ''); 55 | assert.fileContent('addon.xml', ' provider-name="Me">'); 56 | assert.fileContent('addon.xml', 'all'); 57 | assert.fileContent('addon.xml', ''); 58 | }); 59 | it('check contextmenu main.py content', function () { 60 | assert.fileContent('main.py', 'from resources.lib import context'); 61 | assert.fileContent('main.py', 'context.run()'); 62 | assert.noFileContent('main.py', 'from resources.lib import plugin'); 63 | assert.noFileContent('main.py', 'plugin.run(argv=sys.argv)'); 64 | assert.noFileContent('main.py', 'from resources.lib import script'); 65 | assert.noFileContent('main.py', 'script.show_dialog()'); 66 | assert.noFileContent('main.py', 'from resources.lib import service'); 67 | assert.noFileContent('main.py', 'service.run()'); 68 | assert.noFileContent('main.py', 'from resources.lib import subtitle'); 69 | assert.noFileContent('main.py', 'subtitle.run()'); 70 | }); 71 | }); 72 | 73 | describe('generate module', function () { 74 | beforeEach(function () { 75 | return helpers.run(path.join(__dirname, '../generators/app')) 76 | .withPrompts({ 77 | type: 'Module', 78 | scriptid: 'script.module.test', 79 | scriptname: 'My module name', 80 | kodiVersion: '2.24.0', 81 | platforms: 'android', 82 | license: 'MIT', 83 | authors: 'Me, Him', 84 | summary: 'My summary', 85 | authorName: 'My real name', 86 | email: 'test@test.de', 87 | website: 'www.kodi.tv' 88 | }) 89 | .toPromise(); 90 | }); 91 | 92 | it('creates module files', function () { 93 | assert.file([ 94 | 'addon.xml', 95 | '.gitignore', 96 | '.travis.yml', 97 | 'changelog.txt', 98 | 'README.md', 99 | 'LICENSE' 100 | ]); 101 | assert.noFile([ 102 | 'main.py', 103 | 'resources/lib/context.py', 104 | 'resources/lib/plugin.py', 105 | 'resources/lib/service.py', 106 | 'resources/lib/script.py', 107 | 'resources/lib/subtitle.py', 108 | 'tests/README.md', 109 | 'resources/__init__.py', 110 | 'resources/language/resource.language.en_gb/strings.po', 111 | 'resources/language/README.md', 112 | 'resources/lib/__init__.py', 113 | 'resources/lib/kodiutils.py', 114 | 'resources/lib/kodilogging.py', 115 | 'resources/lib/README.md', 116 | 'resources/settings.xml' 117 | ]); 118 | }); 119 | it('check module addon.xml content', function () { 120 | assert.fileContent('addon.xml', ''); 122 | assert.fileContent('addon.xml', ' provider-name="Me, Him">'); 123 | assert.fileContent('addon.xml', 'android'); 124 | assert.fileContent('addon.xml', ''); 125 | }); 126 | }); 127 | 128 | describe('generate plugin', function () { 129 | beforeEach(function () { 130 | return helpers.run(path.join(__dirname, '../generators/app')) 131 | .withPrompts({ 132 | type: 'Plugin', 133 | scriptid: 'plugin.test', 134 | provides: 'video', 135 | scriptname: 'My plugin name', 136 | kodiVersion: '2.25.0', 137 | platforms: 'osx,windx', 138 | license: 'MIT', 139 | authors: 'Me', 140 | summary: 'My summary', 141 | authorName: 'My real name', 142 | email: 'test@test.de', 143 | website: 'www.kodi.tv' 144 | }) 145 | .toPromise(); 146 | }); 147 | 148 | it('creates plugin files', function () { 149 | assert.file([ 150 | 'addon.xml', 151 | '.gitignore', 152 | '.travis.yml', 153 | 'changelog.txt', 154 | 'main.py', 155 | 'README.md', 156 | 'resources/lib/plugin.py', 157 | 'tests/README.md', 158 | 'resources/__init__.py', 159 | 'resources/language/resource.language.en_gb/strings.po', 160 | 'resources/language/README.md', 161 | 'resources/lib/__init__.py', 162 | 'resources/lib/kodiutils.py', 163 | 'resources/lib/kodilogging.py', 164 | 'resources/lib/README.md', 165 | 'resources/settings.xml', 166 | 'LICENSE' 167 | ]); 168 | assert.noFile([ 169 | 'resources/lib/context.py', 170 | 'resources/lib/service.py', 171 | 'resources/lib/script.py', 172 | 'resources/lib/subtitle.py' 173 | ]); 174 | }); 175 | it('check plugin addon.xml content', function () { 176 | assert.fileContent('addon.xml', ''); 178 | assert.fileContent('addon.xml', ' provider-name="Me">'); 179 | assert.fileContent('addon.xml', 'osx windx'); 180 | assert.fileContent('addon.xml', ''); 181 | assert.fileContent('addon.xml', 'video'); 182 | assert.fileContent('addon.xml', ''); 250 | assert.fileContent('addon.xml', ' provider-name="Me">'); 251 | assert.fileContent('addon.xml', 'ios'); 252 | assert.fileContent('addon.xml', ''); 253 | }); 254 | }); 255 | 256 | describe('generate script', function () { 257 | beforeEach(function () { 258 | return helpers.run(path.join(__dirname, '../generators/app')) 259 | .withPrompts({ 260 | type: 'Script', 261 | scriptid: 'script.test', 262 | provides: 'executable', 263 | scriptname: 'My script name', 264 | kodiVersion: '2.25.0', 265 | platforms: 'all', 266 | license: 'MIT', 267 | authors: 'Me, him', 268 | summary: 'My summary', 269 | authorName: 'My real name', 270 | email: 'test@test.de', 271 | website: 'www.kodi.tv' 272 | }) 273 | .toPromise(); 274 | }); 275 | 276 | it('creates script files', function () { 277 | assert.file([ 278 | 'addon.xml', 279 | '.gitignore', 280 | '.travis.yml', 281 | 'changelog.txt', 282 | 'main.py', 283 | 'README.md', 284 | 'resources/lib/script.py', 285 | 'tests/README.md', 286 | 'resources/__init__.py', 287 | 'resources/language/resource.language.en_gb/strings.po', 288 | 'resources/language/README.md', 289 | 'resources/lib/__init__.py', 290 | 'resources/lib/kodiutils.py', 291 | 'resources/lib/kodilogging.py', 292 | 'resources/lib/README.md', 293 | 'resources/settings.xml', 294 | 'LICENSE' 295 | ]); 296 | 297 | assert.noFile([ 298 | 'resources/lib/context.py', 299 | 'resources/libplugin.py', 300 | 'resources/lib/service.py', 301 | 'resources/lib/subtitle.py' 302 | ]); 303 | }); 304 | it('check script addon.xml content', function () { 305 | assert.fileContent('addon.xml', ''); 307 | assert.fileContent('addon.xml', ' provider-name="Me, him">'); 308 | assert.fileContent('addon.xml', 'all'); 309 | assert.fileContent('addon.xml', ''); 310 | assert.fileContent('addon.xml', 'executable'); 311 | assert.noFileContent('addon.xml', ''); 380 | assert.fileContent('addon.xml', ' provider-name="Me">'); 381 | assert.fileContent('addon.xml', 'all'); 382 | assert.fileContent('addon.xml', ''); 383 | assert.noFileContent('addon.xml', ''); 475 | assert.fileContent('addon.xml', ' provider-name="Me">'); 476 | assert.fileContent('addon.xml', 'all'); 477 | assert.fileContent('addon.xml', ''); 478 | }); 479 | it('check subtitle main.py content', function () { 480 | assert.noFileContent('main.py', 'from resources.lib import context'); 481 | assert.noFileContent('main.py', 'context.run()'); 482 | assert.noFileContent('main.py', 'from resources.lib import plugin'); 483 | assert.noFileContent('main.py', 'plugin.run(argv=sys.argv)'); 484 | assert.noFileContent('main.py', 'from resources.lib import script'); 485 | assert.noFileContent('main.py', 'script.show_dialog()'); 486 | assert.noFileContent('main.py', 'from resources.lib import service'); 487 | assert.noFileContent('main.py', 'service.run()'); 488 | assert.fileContent('main.py', 'from resources.lib import subtitle'); 489 | assert.fileContent('main.py', 'subtitle.run()'); 490 | }); 491 | }); 492 | -------------------------------------------------------------------------------- /__tests__/validationHelper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var assert = require('yeoman-assert'); 3 | var helper = require('../generators/app/validationHelper'); 4 | 5 | describe('prompting validations → validateContextmenuName()', () => { 6 | it('should work for context.test', () => { 7 | assert.equal(helper.validateContextmenuName('context.test'), true); 8 | }); 9 | it('should fail when to short', () => { 10 | assert.equal(helper.validateContextmenuName('contex'), false); 11 | }); 12 | }); 13 | describe('prompting validations → validateModuleName()', () => { 14 | it('should work for script.module.test', () => { 15 | assert.equal(helper.validateModuleName('script.module.test'), true); 16 | }); 17 | it('should fail when to short', () => { 18 | assert.equal(helper.validateModuleName('modu'), false); 19 | }); 20 | }); 21 | describe('prompting validations → validatePluginName()', () => { 22 | it('should work for plugin.test', () => { 23 | assert.equal(helper.validatePluginName('plugin.test'), true); 24 | }); 25 | it('should fail when to short', () => { 26 | assert.equal(helper.validatePluginName('plugin'), false); 27 | }); 28 | }); 29 | describe('prompting validations → validateResourceName()', () => { 30 | it('should work for resource.test', () => { 31 | assert.equal(helper.validateResourceName('resource.test'), true); 32 | }); 33 | it('should fail when to short', () => { 34 | assert.equal(helper.validateResourceName('resource'), false); 35 | }); 36 | }); 37 | describe('prompting validations → validateServiceName()', () => { 38 | it('should work for service.test', () => { 39 | assert.equal(helper.validateServiceName('service.test'), true); 40 | }); 41 | it('should fail when to short', () => { 42 | assert.equal(helper.validateServiceName('service'), false); 43 | }); 44 | }); 45 | describe('prompting validations → validateScriptName()', () => { 46 | it('should work for script.test', () => { 47 | assert.equal(helper.validateScriptName('script.test'), true); 48 | }); 49 | it('should fail when to short', () => { 50 | assert.equal(helper.validateScriptName('script'), false); 51 | }); 52 | }); 53 | describe('prompting validations → validateSubtitleName()', () => { 54 | it('should work for service.subtitles.test', () => { 55 | assert.equal(helper.validateSubtitleName('service.subtitles.test'), true); 56 | }); 57 | it('should fail when to short', () => { 58 | assert.equal(helper.validateSubtitleName('service.subtitles'), false); 59 | }); 60 | }); 61 | describe('prompting validations → validateScriptNameLength()', () => { 62 | it('should work for anything longer than two letters', () => { 63 | assert.equal(helper.validateScriptNameLength('My name'), true); 64 | }); 65 | it('should fail when to short', () => { 66 | assert.equal(helper.validateScriptNameLength('my'), false); 67 | }); 68 | }); 69 | describe('prompting validations → validateProvides()', () => { 70 | it('should work when selecting more then one item', () => { 71 | assert.equal(helper.validateProvides(['video', 'audio']), true); 72 | }); 73 | it('should work when selecting only one item', () => { 74 | assert.equal(helper.validateProvides(['video']), true); 75 | }); 76 | it('should fail when nothing is selected', () => { 77 | assert.equal(helper.validateProvides([]), 'You need check at least one.'); 78 | }); 79 | }); 80 | describe('prompting validations → validatePlatforms()', () => { 81 | it('should work when selecting more then one item', () => { 82 | assert.equal(helper.validatePlatforms(['osx', 'android']), true); 83 | }); 84 | it('should work when selecting only one item', () => { 85 | assert.equal(helper.validatePlatforms(['all']), true); 86 | }); 87 | it('should fail when all and something else is selected', () => { 88 | assert.equal(helper.validatePlatforms(['all', 'osx']), '"All" must be the only platform selected.'); 89 | }); 90 | it('should fail when nothing is selected', () => { 91 | assert.equal(helper.validatePlatforms([]), 'You need check at least one.'); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /generators/app/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var chalk = require('chalk'); 3 | var mkdirp = require('mkdirp'); 4 | var path = require('path'); 5 | var yeoman = require('yeoman-generator'); 6 | var yosay = require('yosay'); 7 | var helper = require('./validationHelper'); 8 | 9 | module.exports = class extends yeoman { 10 | prompting() { 11 | // Have Yeoman greet the user. 12 | this.log(yosay( 13 | 'Welcome to the awesome ' + chalk.red('generator-kodi') + ' generator!' 14 | )); 15 | 16 | var prompts = [{ 17 | type: 'list', 18 | name: 'type', 19 | message: 'Choose the type of addon you want to create.', 20 | choices: ['Contextmenu', 'Module', 'Plugin', 'Resource', 'Script', 'Service', 'Subtitle'], 21 | default: 0 22 | }]; 23 | 24 | return this.prompt(prompts).then(function (props) { 25 | // To access props later use this.props.someAnswer; 26 | this.props = props; 27 | }.bind(this)); 28 | } 29 | 30 | askForAddonData() { 31 | var prompts = []; 32 | 33 | var kodiVersion = [{ 34 | name: 'Krypton', 35 | value: '2.25.0' 36 | }, { 37 | name: 'Jarvis', 38 | value: '2.24.0' 39 | }]; 40 | 41 | if (this.props.type == 'Plugin' || this.props.type == 'Script') { 42 | prompts.push({ 43 | type: 'checkbox', 44 | name: 'provides', 45 | message: 'Your addon will provide the following media types.', 46 | choices: ['audio', 'image', 'executable', 'game', 'video'], 47 | validate: helper.validateProvides 48 | }); 49 | } 50 | 51 | if (this.props.type == 'Service') { 52 | prompts.push({ 53 | type: 'list', 54 | name: 'start', 55 | message: 'Your service should start at:', 56 | choices: ['login', 'startup'] 57 | }); 58 | } 59 | 60 | var scriptidMessage; 61 | var validationHelper; 62 | if (this.props.type == 'Contextmenu') { 63 | scriptidMessage = 'Your addon id, it should be in the format context.name.name and not contain spaces. (for e.g. context.hello.menu)'; 64 | validationHelper = helper.validateContextmenuName; 65 | } else if (this.props.type == 'Module') { 66 | scriptidMessage = 'Your addon id, it should be in the format script.module.name and not contain spaces. (for e.g. script.module.hello)'; 67 | validationHelper = helper.validateModuleName; 68 | } else if (this.props.type == 'Plugin') { 69 | scriptidMessage = 'Your addon id, it should be in the format plugin.name and not contain spaces. (for e.g. plugin.test.hello)'; 70 | validationHelper = helper.validatePluginName; 71 | } else if (this.props.type == 'Resource') { 72 | scriptidMessage = 'Your addon id, it should be in the format resource.resourcetype.name and not contain spaces. (for e.g. resource.images.hello)'; 73 | validationHelper = helper.validateResourceName; 74 | } else if (this.props.type == 'Service') { 75 | scriptidMessage = 'Your addon id, it should be in the format service.name and not contain spaces. (for e.g. service.test.hello)'; 76 | validationHelper = helper.validateServiceName; 77 | } else if (this.props.type == 'Script') { 78 | scriptidMessage = 'Your addon id, it should be in the format script.name and not contain spaces. (for e.g. script.test.hello)'; 79 | validationHelper = helper.validateScriptName; 80 | } else if (this.props.type == 'Subtitle') { 81 | scriptidMessage = 'Your addon id, it should be in the format service.subtitles.name and not contain spaces. (for e.g. service.subtitles.hello)'; 82 | validationHelper = helper.validateSubtitleName; 83 | } 84 | 85 | prompts.push({ 86 | type: 'input', 87 | name: 'scriptid', 88 | message: scriptidMessage, 89 | validate: validationHelper 90 | }, { 91 | type: 'input', 92 | name: 'scriptname', 93 | message: 'Your addon name, it should be easily readable. (for e.g. Hello World)', 94 | validate: helper.validateScriptNameLength 95 | }, { 96 | type: 'list', 97 | name: 'kodiVersion', 98 | message: 'Choose the minimal Kodi Version your targeting.', 99 | choices: kodiVersion, 100 | default: 0 101 | }, { 102 | type: 'checkbox', 103 | name: 'platforms', 104 | message: 'Which platforms does this run with?', 105 | choices: ['all', 'android', 'linux', 'ios', 'osx', 'windx'], 106 | validate: helper.validatePlatforms 107 | }, { 108 | type: 'list', 109 | name: 'license', 110 | message: 'Choose your license.', 111 | choices: require('generator-license').licenses, 112 | default: 0 113 | }, { 114 | type: 'input', 115 | name: 'authors', 116 | message: 'All author names? (seperated by ,)' 117 | }, { 118 | type: 'input', 119 | name: 'summary', 120 | message: 'What does your addon do?' 121 | }, { 122 | type: 'input', 123 | name: 'authorName', 124 | message: 'Your real name? We are using this for the license creation.' 125 | }, { 126 | type: 'input', 127 | name: 'email', 128 | message: 'Your email address? (for e.g. john.doe@gmail.com)' 129 | }, { 130 | type: 'input', 131 | name: 'website', 132 | message: 'Your website URL? (for e.g. www.kodi.tv)' 133 | }); 134 | 135 | return this.prompt(prompts).then(function (props) { 136 | // To access props later use this.props.someAnswer; 137 | this.props = Object.assign(this.props, props); 138 | }.bind(this)); 139 | } 140 | 141 | default() { 142 | if (path.basename(this.destinationPath()) !== this.props.scriptid) { 143 | this.log( 144 | 'Your addon must be inside a folder named ' + this.props.scriptid + '\n' + 145 | 'I\'ll automatically create this folder.' 146 | ); 147 | mkdirp(this.props.scriptid); 148 | this.destinationRoot(this.destinationPath(this.props.scriptid)); 149 | } 150 | } 151 | 152 | writing() { 153 | this.fs.copyTpl( 154 | this.templatePath('addon.xml'), 155 | this.destinationPath('addon.xml'), { 156 | props: this.props, 157 | platforms: this.props.platforms === undefined ? this.props.platforms : this.props.platforms.toString().replace(/[,]/g, ' '), 158 | provides: this.props.provides === undefined ? this.props.provides : this.props.provides.toString().replace(/[,]/g, ' ') 159 | } 160 | ); 161 | this.fs.copy( 162 | this.templatePath('gitignore'), 163 | this.destinationPath('.gitignore') 164 | ); 165 | this.fs.copy( 166 | this.templatePath('travis.yml'), 167 | this.destinationPath('.travis.yml') 168 | ); 169 | this.fs.copy( 170 | this.templatePath('changelog.txt'), 171 | this.destinationPath('changelog.txt') 172 | ); 173 | this.fs.copy( 174 | this.templatePath('README.md'), 175 | this.destinationPath('README.md') 176 | ); 177 | 178 | if (this.props.type != 'Module' && this.props.type != 'Resource') { 179 | this.fs.copyTpl( 180 | this.templatePath('main.py'), 181 | this.destinationPath('main.py'), { 182 | props: this.props 183 | } 184 | ); 185 | } 186 | 187 | if (this.props.type == 'Contextmenu') { 188 | this.fs.copy( 189 | this.templatePath('resources/lib/context.py'), 190 | this.destinationPath('resources/lib/context.py') 191 | ); 192 | } else if (this.props.type == 'Plugin') { 193 | this.fs.copy( 194 | this.templatePath('resources/lib/plugin.py'), 195 | this.destinationPath('resources/lib/plugin.py') 196 | ); 197 | } else if (this.props.type == 'Script') { 198 | this.fs.copy( 199 | this.templatePath('resources/lib/script.py'), 200 | this.destinationPath('resources/lib/script.py') 201 | ); 202 | } else if (this.props.type == 'Service') { 203 | this.fs.copy( 204 | this.templatePath('resources/lib/service.py'), 205 | this.destinationPath('resources/lib/service.py') 206 | ); 207 | } else if (this.props.type == 'Subtitle') { 208 | this.fs.copy( 209 | this.templatePath('resources/lib/subtitle.py'), 210 | this.destinationPath('resources/lib/subtitle.py') 211 | ); 212 | } 213 | 214 | if (this.props.type != 'Module' && this.props.type != 'Resource' && this.props.type != 'Contextmenu') { 215 | this.fs.copy( 216 | this.templatePath('tests/**'), 217 | this.destinationPath('tests/') 218 | ); 219 | } 220 | 221 | if (this.props.type != 'Module' && this.props.type != 'Resource') { 222 | if (this.props.type == 'Contextmenu') { 223 | this.fs.copy( 224 | this.templatePath('resources/language/**'), 225 | this.destinationPath('resources/language/') 226 | ); 227 | } else { 228 | this.fs.copyTpl( 229 | this.templatePath('resources/*'), 230 | this.destinationPath('resources/'), { 231 | props: this.props 232 | } 233 | ); 234 | this.fs.copyTpl( 235 | this.templatePath('resources/language/**'), 236 | this.destinationPath('resources/language/'), { 237 | props: this.props 238 | } 239 | ); 240 | this.fs.copyTpl( 241 | this.templatePath('resources/lib/__init__.py'), 242 | this.destinationPath('resources/lib/__init__.py'), { 243 | props: this.props 244 | } 245 | ); 246 | this.fs.copyTpl( 247 | this.templatePath('resources/lib/kodiutils.py'), 248 | this.destinationPath('resources/lib/kodiutils.py'), { 249 | props: this.props 250 | } 251 | ); 252 | this.fs.copyTpl( 253 | this.templatePath('resources/lib/kodilogging.py'), 254 | this.destinationPath('resources/lib/kodilogging.py'), { 255 | props: this.props 256 | } 257 | ); 258 | this.fs.copyTpl( 259 | this.templatePath('resources/lib/README.md'), 260 | this.destinationPath('resources/lib/README.md'), { 261 | props: this.props 262 | } 263 | ); 264 | } 265 | } 266 | 267 | if (this.props.type == 'Resource') { 268 | mkdirp(this.destinationPath('resources/')); 269 | } 270 | 271 | this.composeWith(require.resolve('generator-license/app'), { 272 | name: this.props.authorName, 273 | email: this.props.email, 274 | website: this.props.website, 275 | license: this.props.license 276 | }); 277 | this.composeWith(require.resolve('generator-git-init'), { 278 | commit: 'Created initial addon structure' 279 | }); 280 | } 281 | }; 282 | -------------------------------------------------------------------------------- /generators/app/templates/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your addon 2 | 3 | 1. You might want to move this folder into the kodi addon folder for convinience when debugging. It might also be needed to be `enabled` inside of the kodi addon browser. 4 | 2. Now start coding! Just open up the `.py` file in this folder and create what you would like Kodi to do! If you're creating a plugin, please check out [this kodi routing framework](https://github.com/tamland/kodi-plugin-routing) and copy a version of that module to your kodi addon folder. 5 | 3. Write some tests, maybe? Don't forget to activate [travis](https://travis-ci.org/) access to your repository. We've created a test folder and a travis config file for that, otherwise just delete those ;) 6 | 4. You might want to look at your `addon.xml` it should already be filled, but you will need to understand what your doing and might want to fill in some more info. So read up [here](http://kodi.wiki/view/Addon.xml). 7 | 5. Do you want some settings for your addon? Check the `settings.xml` in the resources folder. And read up [here](http://kodi.wiki/view/Settings.xml). 8 | 6. Read [this info](http://kodi.wiki/view/Add-on_structure#icon.png) and drop an icon for your addon into the `resource` folder and name it `icon.png`. 9 | 7. Read [this](http://kodi.wiki/view/Add-on_structure#fanart.jpg) and drop a addon background into the `resource` folder and name it `fanart.jpg`. 10 | 8. End up with a beautiful Kodi addon! Good for you :) Maybe you want to [share it with us](http://kodi.wiki/view/Submitting_Add-on_updates_on_Github)? 11 | 12 | ### Debugging 13 | To get the debug logging to work, just set the global kodi logging to true and the debug logging in your addons settings. -------------------------------------------------------------------------------- /generators/app/templates/addon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%_ if (props.type == 'Plugin') { -%> <% } %> 6 | <%_ if (props.kodiVersion == '2.24.0' && (props.type == 'Plugin' || props.type == 'Script' || props.type == 'Service')) { -%> <% } %> 7 | 8 | <% if (props.type == 'Contextmenu') { -%> 9 | 10 | 11 | 12 | True 13 | 14 | <% } -%><% if (props.type == 'Module') {%><% } %><% if (props.type == 'Plugin') {%> 15 | <%= provides %> 16 | <% } %><% if (props.type == 'Resource') {%><% } %><% if (props.type == 'Script') {%> 17 | <%= provides %> 18 | <% } %><% if (props.type == 'Service') {%><% } %><% if (props.type == 'Subtitle') {%><% } %> 19 | 20 | <%= props.summary %> 21 | 22 | 23 | <%= platforms %> 24 | <%= props.license %> 25 | 26 | <%= props.website %> 27 | <%= props.email %> 28 | 29 | 30 | 31 | 32 | resources/icon.png 33 | resources/fanart.jpg 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /generators/app/templates/changelog.txt: -------------------------------------------------------------------------------- 1 | v0.0.1 2 | - Initial version -------------------------------------------------------------------------------- /generators/app/templates/gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | .DS* 4 | .pylint_rc 5 | /.idea 6 | /.project 7 | /.pydevproject 8 | /.settings 9 | Thumbs.db 10 | *~ 11 | .cache 12 | -------------------------------------------------------------------------------- /generators/app/templates/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | <%_ if (props.type == 'Plugin') { -%> 4 | import sys 5 | <% } %> 6 | 7 | from resources.lib import kodilogging 8 | <%_ if (props.type == 'Contextmenu') { -%> 9 | from resources.lib import context 10 | <%_ } else if (props.type == 'Plugin') { -%> 11 | from resources.lib import plugin 12 | <%_ } else if (props.type == 'Script') { -%> 13 | from resources.lib import script 14 | <%_ } else if (props.type == 'Service') { -%> 15 | from resources.lib import service 16 | <%_ } else if (props.type == 'Subtitle') { -%> 17 | from resources.lib import subtitle 18 | <% } %> 19 | 20 | import xbmcaddon 21 | # Keep this file to a minimum, as Kodi 22 | # doesn't keep a compiled copy of this 23 | ADDON = xbmcaddon.Addon() 24 | kodilogging.config() 25 | 26 | <%_ if (props.type == 'Contextmenu') { -%> 27 | context.run() 28 | <%_ } else if (props.type == 'Plugin') { -%> 29 | plugin.run(argv=sys.argv) 30 | <%_ } else if (props.type == 'Script') { -%> 31 | script.show_dialog() 32 | <%_ } else if (props.type == 'Service') { -%> 33 | service.run() 34 | <%_ } else if (props.type == 'Subtitle') { -%> 35 | subtitle.run() 36 | <% } %> 37 | 38 | -------------------------------------------------------------------------------- /generators/app/templates/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xbmc/generator-kodi-addon/b5a70a4969c7eac339008ba7fc02b1cbdff082a4/generators/app/templates/resources/__init__.py -------------------------------------------------------------------------------- /generators/app/templates/resources/language/README.md: -------------------------------------------------------------------------------- 1 | This folder will be the home of your translations -------------------------------------------------------------------------------- /generators/app/templates/resources/language/resource.language.en_gb/strings.po: -------------------------------------------------------------------------------- 1 | # Kodi Media Center language file 2 | # Addon Name: <%= props.scriptname %> 3 | # Addon id: <%= props.scriptid %> 4 | # Addon Provider: <%= props.authors %> 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: XBMC Addons\n" 8 | "Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" 9 | "POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" 10 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 11 | "Last-Translator: Kodi Translation Team\n" 12 | "Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Language: en\n" 17 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 18 | 19 | msgctxt "#32000" 20 | msgid "Example" 21 | msgstr "" 22 | 23 | msgctxt "#32001" 24 | msgid "Debug" 25 | msgstr "" -------------------------------------------------------------------------------- /generators/app/templates/resources/lib/README.md: -------------------------------------------------------------------------------- 1 | # Readme 2 | This folder will be the home of your python files, that are not entry points. 3 | So put your `.py` files here. 4 | 5 | # Conventions 6 | There's also a convention in place here, prefix every file kodi specific (e.g. importing `xbmcaddon` or `xbmcgui`) with `kodi`. Like we're doing with `kodiutils.py`. 7 | Move all functions that not need kodi specifics into their own files, without the kodi prefix. This will make them unit testable. -------------------------------------------------------------------------------- /generators/app/templates/resources/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xbmc/generator-kodi-addon/b5a70a4969c7eac339008ba7fc02b1cbdff082a4/generators/app/templates/resources/lib/__init__.py -------------------------------------------------------------------------------- /generators/app/templates/resources/lib/context.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import xbmcaddon 4 | import xbmcgui 5 | 6 | 7 | def run(): 8 | # Implement what your contextmenu aims to do here 9 | # For example you could call executebuiltin to call another addon 10 | # xbmc.executebuiltin("RunScript(script.example,action=show)") 11 | # You might want to check your addon.xml for the visible condition of your contextmenu 12 | # Read more here http://kodi.wiki/view/Context_Item_Add-ons 13 | addon = xbmcaddon.Addon() 14 | addon_name = addon.getAddonInfo('name') 15 | 16 | line1 = "Hello World!" 17 | 18 | xbmcgui.Dialog().ok(addon_name, line1) 19 | -------------------------------------------------------------------------------- /generators/app/templates/resources/lib/kodilogging.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | from resources.lib.kodiutils import get_setting_as_bool 5 | 6 | import logging 7 | import xbmc 8 | import xbmcaddon 9 | 10 | 11 | class KodiLogHandler(logging.StreamHandler): 12 | 13 | def __init__(self): 14 | logging.StreamHandler.__init__(self) 15 | addon_id = xbmcaddon.Addon().getAddonInfo("id") 16 | formatter = logging.Formatter("[{}] %(name)s %(message)s".format(addon_id)) 17 | self.setFormatter(formatter) 18 | 19 | def emit(self, record): 20 | levels = { 21 | logging.CRITICAL: xbmc.LOGFATAL, 22 | logging.ERROR: xbmc.LOGERROR, 23 | logging.WARNING: xbmc.LOGWARNING, 24 | logging.INFO: xbmc.LOGINFO, 25 | logging.DEBUG: xbmc.LOGDEBUG, 26 | logging.NOTSET: xbmc.LOGNONE, 27 | } 28 | if get_setting_as_bool('debug'): 29 | try: 30 | xbmc.log(self.format(record), levels[record.levelno]) 31 | except UnicodeEncodeError: 32 | xbmc.log(self.format(record).encode( 33 | 'utf-8', 'ignore'), levels[record.levelno]) 34 | 35 | def flush(self): 36 | pass 37 | 38 | 39 | def config(): 40 | logger = logging.getLogger() 41 | logger.addHandler(KodiLogHandler()) 42 | logger.setLevel(logging.DEBUG) 43 | -------------------------------------------------------------------------------- /generators/app/templates/resources/lib/kodiutils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import xbmc 4 | import xbmcaddon 5 | import xbmcgui 6 | import sys 7 | import logging 8 | <%_ if (props.kodiVersion == '2.24.0') { -%> 9 | if sys.version_info >= (2, 7): 10 | import json as json 11 | else: 12 | import simplejson as json 13 | <%_ } else { -%> 14 | import json as json 15 | <% } %> 16 | 17 | # read settings 18 | ADDON = xbmcaddon.Addon() 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | def notification(header, message, time=5000, icon=ADDON.getAddonInfo('icon'), sound=True): 24 | xbmcgui.Dialog().notification(header, message, icon, time, sound) 25 | 26 | 27 | def show_settings(): 28 | ADDON.openSettings() 29 | 30 | 31 | def get_setting(setting): 32 | return ADDON.getSetting(setting).strip().decode('utf-8') 33 | 34 | 35 | def set_setting(setting, value): 36 | ADDON.setSetting(setting, str(value)) 37 | 38 | 39 | def get_setting_as_bool(setting): 40 | return ADDON.getSettingBool(setting) 41 | 42 | 43 | def get_setting_as_float(setting): 44 | try: 45 | return ADDON.getSettingNumber(setting) 46 | except ValueError: 47 | return 0 48 | 49 | 50 | def get_setting_as_int(setting): 51 | try: 52 | return ADDON.getSettingInt(setting) 53 | except ValueError: 54 | return 0 55 | 56 | 57 | def get_string(string_id): 58 | return ADDON.getLocalizedString(string_id).encode('utf-8', 'ignore') 59 | 60 | 61 | def kodi_json_request(params): 62 | data = json.dumps(params) 63 | request = xbmc.executeJSONRPC(data) 64 | 65 | try: 66 | response = json.loads(request) 67 | except UnicodeDecodeError: 68 | response = json.loads(request.decode('utf-8', 'ignore')) 69 | 70 | try: 71 | if 'result' in response: 72 | return response['result'] 73 | return None 74 | except KeyError: 75 | logger.warn("[{}] {}".format(params['method'], response['error']['message'])) 76 | return None 77 | -------------------------------------------------------------------------------- /generators/app/templates/resources/lib/plugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import routing 4 | import logging 5 | import xbmcaddon 6 | from resources.lib import kodiutils 7 | from resources.lib import kodilogging 8 | from xbmcgui import ListItem 9 | from xbmcplugin import addDirectoryItem, endOfDirectory 10 | 11 | 12 | ADDON = xbmcaddon.Addon() 13 | logger = logging.getLogger(ADDON.getAddonInfo('id')) 14 | kodilogging.config() 15 | plugin = routing.Plugin() 16 | 17 | 18 | @plugin.route('/') 19 | def index(): 20 | addDirectoryItem(plugin.handle, plugin.url_for( 21 | show_category, "one"), ListItem("Category One"), True) 22 | addDirectoryItem(plugin.handle, plugin.url_for( 23 | show_category, "two"), ListItem("Category Two"), True) 24 | endOfDirectory(plugin.handle) 25 | 26 | 27 | @plugin.route('/category/') 28 | def show_category(category_id): 29 | addDirectoryItem( 30 | plugin.handle, "", ListItem("Hello category %s!" % category_id)) 31 | endOfDirectory(plugin.handle) 32 | 33 | def run(argv): 34 | plugin.run(argv=argv) 35 | -------------------------------------------------------------------------------- /generators/app/templates/resources/lib/script.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from resources.lib import kodiutils 4 | from resources.lib import kodilogging 5 | import logging 6 | import xbmcaddon 7 | import xbmcgui 8 | 9 | 10 | ADDON = xbmcaddon.Addon() 11 | logger = logging.getLogger(ADDON.getAddonInfo('id')) 12 | 13 | 14 | # Put your code here, this is just an example showing 15 | # a textbox as soon as this addon gets called 16 | def show_dialog(): 17 | addon_name = ADDON.getAddonInfo('name') 18 | 19 | line1 = "Hello World!" 20 | 21 | xbmcgui.Dialog().ok(addon_name, line1) 22 | -------------------------------------------------------------------------------- /generators/app/templates/resources/lib/service.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from resources.lib import kodiutils 4 | from resources.lib import kodilogging 5 | import logging 6 | import time 7 | import xbmc 8 | import xbmcaddon 9 | 10 | 11 | ADDON = xbmcaddon.Addon() 12 | logger = logging.getLogger(ADDON.getAddonInfo('id')) 13 | 14 | 15 | def run(): 16 | monitor = xbmc.Monitor() 17 | 18 | while not monitor.abortRequested(): 19 | # Sleep/wait for abort for 10 seconds 20 | if monitor.waitForAbort(10): 21 | # Abort was requested while waiting. We should exit 22 | break 23 | logger.debug("hello addon! %s" % time.time()) 24 | -------------------------------------------------------------------------------- /generators/app/templates/resources/lib/subtitle.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import urllib2 4 | import sys 5 | import urlparse 6 | import urllib 7 | import os 8 | import unicodedata 9 | import xbmcgui 10 | import xbmcplugin 11 | import xbmc 12 | import xbmcaddon 13 | import xbmcvfs 14 | 15 | ADDON = xbmcaddon.Addon() 16 | SCRIPT_ID = ADDON.getAddonInfo('id') 17 | PROFILE = xbmc.translatePath(ADDON.getAddonInfo('profile')) 18 | TEMP = os.path.join(PROFILE, 'temp', '') 19 | HANDLE = int(sys.argv[1]) 20 | 21 | # Create the folder where we will store the downloaded subtitles 22 | if not xbmcvfs.exists(TEMP): 23 | xbmcvfs.mkdirs(TEMP) 24 | 25 | # Function to retrieve passed parameters, returns a dict 26 | def get_params(): 27 | if len(sys.argv) > 2: 28 | return dict(urlparse.parse_qsl(sys.argv[2].lstrip('?'))) 29 | return {} 30 | 31 | def normalize_string(str): 32 | return unicodedata.normalize( 33 | 'NFKD', unicode(unicode(str, 'utf-8')) 34 | ).encode('ascii', 'ignore') 35 | 36 | # Gather all the information available from the playing item, 37 | # helps to determine what subtitle to list 38 | def get_info(): 39 | item = {} 40 | item['temp'] = False 41 | item['rar'] = False 42 | # Year 43 | item['year'] = xbmc.getInfoLabel("VideoPlayer.Year") 44 | # Season 45 | item['season'] = str(xbmc.getInfoLabel("VideoPlayer.Season")) 46 | # Episode 47 | item['episode'] = str(xbmc.getInfoLabel("VideoPlayer.Episode")) 48 | # Show title 49 | item['tvshow'] = normalize_string( 50 | xbmc.getInfoLabel("VideoPlayer.TVshowtitle")) 51 | # try to get original title 52 | item['title'] = normalize_string( 53 | xbmc.getInfoLabel("VideoPlayer.OriginalTitle")) 54 | # Full path of a playing file 55 | item['file_original_path'] = urllib.unquote( 56 | xbmc.Player().getPlayingFile().decode('utf-8')) 57 | 58 | if item['title'] == "": 59 | # no original title, get just Title 60 | item['title'] = normalize_string(xbmc.getInfoLabel("VideoPlayer.Title")) 61 | 62 | # Check if season is "Special" 63 | if item['episode'].lower().find("s") > -1: 64 | item['season'] = "0" 65 | item['episode'] = item['episode'][-1:] 66 | 67 | # Check if the path is not a local one 68 | if (item['file_original_path'].find("http") > -1): 69 | item['temp'] = True 70 | 71 | # Check if the path is to a rar file 72 | elif (item['file_original_path'].find("rar://") > -1): 73 | item['rar'] = True 74 | item['file_original_path'] = os.path.dirname( 75 | item['file_original_path'][6:]) 76 | 77 | # Check if the path is part of a stack of files 78 | elif (item['file_original_path'].find("stack://") > -1): 79 | stackPath = item['file_original_path'].split(" , ") 80 | item['file_original_path'] = stackPath[0][8:] 81 | 82 | # Filename 83 | item['filename'] = os.path.splitext( 84 | os.path.basename(item['file_original_path']))[0] 85 | return item 86 | 87 | # Get the requested languages the user want to search (3 letter format) 88 | def get_languages(params): 89 | langs = [] # ['scc','eng'] 90 | for lang in urllib.unquote(params['languages']).decode('utf-8').split(","): 91 | langs.append(xbmc.convertLanguage(lang, xbmc.ISO_639_2)) 92 | return langs 93 | 94 | # Add the subtitle to the list of subtitles to show 95 | def append_subtitle(subname, lang_name, language, params, sync=False, h_impaired=False): 96 | listitem = xbmcgui.ListItem( 97 | # Languange name to display under the lang logo (for example english) 98 | label=lang_name, 99 | # Subtitle name (for example 'Lost 1x01 720p') 100 | label2=subname, 101 | # Languange 2 letter name (for example en) 102 | thumbnailImage=xbmc.convertLanguage(language, xbmc.ISO_639_1)) 103 | 104 | # Subtitles synced with the video 105 | listitem.setProperty("sync", 'true' if sync else 'false') 106 | # Hearing impaired subs 107 | listitem.setProperty("hearing_imp", 'true' if h_impaired else 'false') 108 | 109 | # Create the url to the plugin that will handle the subtitle download 110 | url = "plugin://{url}/?{params}".format( 111 | url=SCRIPT_ID, params=urllib.urlencode(params)) 112 | # Add the subtitle to the list 113 | xbmcplugin.addDirectoryItem( 114 | handle=HANDLE, url=url, listitem=listitem, isFolder=False) 115 | 116 | # Add subtitles to the list using information gathered with get_info and get_languages 117 | def search(info, languages): 118 | append_subtitle( 119 | "Lost 1x01", "English", "eng", {"action": "download", "id": 15}, sync=True) 120 | append_subtitle("Lost 1x01", "Italian", "ita", {"action": "download", "id": 16}) 121 | append_subtitle("Lost 1x01 720p", "English", "eng", {"action": "download", "id": 17}) 122 | 123 | # Add subtitles to the list using user manually inserted string 124 | def manual_search(search_str, languages): 125 | append_subtitle( 126 | "Lost 1x01", "English", "eng", {"action": "download", "id": 15}, sync=True) 127 | append_subtitle("Lost 1x01", "Italian", "ita", {"action": "download", "id": 16}) 128 | append_subtitle("Lost 1x01 720p", "English", "eng", {"action": "download", "id": 17}) 129 | 130 | # download the subtitle chosen by the user 131 | def download(params): 132 | id = params['id'] 133 | # download the file requested 134 | url = "http://path.to/subtitle/{id}.srt".format(id=id) 135 | file = os.path.join(TEMP, "{id}.srt".format(id=id)) 136 | 137 | response = urllib2.urlopen(url) 138 | with open(file, "w") as local_file: 139 | local_file.write(response.read()) 140 | 141 | # give the file to kodi 142 | xbmcplugin.addDirectoryItem( 143 | handle=HANDLE, url=file, listitem=xbmcgui.ListItem(label=file), isFolder=False) 144 | 145 | def run(): 146 | # Gather the request info 147 | params = get_params() 148 | 149 | if 'action' in params: 150 | if params['action'] == "search": 151 | # If the action is 'search' use item information kodi provides to search for subtitles 152 | search(get_info(), get_languages(params)) 153 | elif params['action'] == "manualsearch": 154 | # If the action is 'manualsearch' use user manually inserted string to search for subtitles 155 | manual_search(params['searchstring'], get_languages(params)) 156 | elif params['action'] == "download": 157 | # If the action is 'download' use the info provided to download the subtitle and give the file path to kodi 158 | download(params) 159 | 160 | xbmcplugin.endOfDirectory(HANDLE) 161 | -------------------------------------------------------------------------------- /generators/app/templates/resources/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /generators/app/templates/tests/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | This folder should be the home for your unit tests -------------------------------------------------------------------------------- /generators/app/templates/travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | matrix: 3 | include: 4 | - python: "2.6" 5 | - python: "2.7" 6 | - python: "2.7.10" 7 | - python: "2.7.11" 8 | allow_failures: 9 | - python: "3.2" 10 | - python: "3.3" 11 | - python: "3.4" 12 | - python: "3.5" 13 | - python: "nightly" 14 | 15 | # command to install dependencies 16 | install: 17 | - pip install python-dateutil pytest 18 | # command to run tests 19 | script: py.test -v 20 | 21 | -------------------------------------------------------------------------------- /generators/app/validationHelper.js: -------------------------------------------------------------------------------- 1 | const helper = {}; 2 | 3 | helper.validateContextmenuName = function (str) { 4 | return str.length > 'context.'.length; 5 | }; 6 | 7 | helper.validateModuleName = function (str) { 8 | return str.length > 'script.module.'.length; 9 | }; 10 | 11 | helper.validatePluginName = function (str) { 12 | return str.length > 'plugin.'.length; 13 | }; 14 | 15 | helper.validateResourceName = function (str) { 16 | return str.length > 'resource.'.length; 17 | }; 18 | 19 | helper.validateServiceName = function (str) { 20 | return str.length > 'service.'.length; 21 | }; 22 | 23 | helper.validateScriptName = function (str) { 24 | return str.length > 'script.'.length; 25 | }; 26 | 27 | helper.validateSubtitleName = function (str) { 28 | return str.length > 'service.subtitles.'.length; 29 | }; 30 | 31 | helper.validateScriptNameLength = function (str) { 32 | return str.length > 2; 33 | }; 34 | 35 | helper.validateProvides = function (provides) { 36 | if (provides.length < 1) { 37 | return 'You need check at least one.'; 38 | } 39 | return true; 40 | }; 41 | 42 | helper.validatePlatforms = function (platforms) { 43 | if (platforms.indexOf('all') != -1 && platforms.length > 1) { 44 | return '"All" must be the only platform selected.'; 45 | } 46 | if (platforms.length < 1) { 47 | return 'You need check at least one.'; 48 | } 49 | return true; 50 | }; 51 | 52 | module.exports = helper; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "generator-kodi-addon", 3 | "version": "0.0.7", 4 | "description": "Creates the basic structure for a kodi addon, written in python.", 5 | "homepage": "https://github.com/xbmc/generator-kodi-addon#installation", 6 | "author": { 7 | "name": "Kolja Lampe", 8 | "email": "razzeee@gmail.com", 9 | "url": "" 10 | }, 11 | "files": [ 12 | "generators" 13 | ], 14 | "main": "generators/index.js", 15 | "keywords": [ 16 | "kodi", 17 | "xbmc", 18 | "python", 19 | "script", 20 | "yeoman-generator" 21 | ], 22 | "dependencies": { 23 | "chalk": "^2.4.1", 24 | "generator-git-init": "^1.1.3", 25 | "generator-license": "^5.4.0", 26 | "mkdirp": "^0.5.1", 27 | "yeoman-generator": "^3.1.1", 28 | "yosay": "^2.0.2" 29 | }, 30 | "devDependencies": { 31 | "coveralls": "^3.0.2", 32 | "eslint": "^5.5.0", 33 | "eslint-config-xo-space": "^0.20.0", 34 | "jest": "^23.5.0", 35 | "jest-cli": "^23.5.0", 36 | "nsp": "^3.2.1", 37 | "yeoman-assert": "^3.1.1", 38 | "yeoman-test": "^1.9.1" 39 | }, 40 | "jest": { 41 | "testEnvironment": "node" 42 | }, 43 | "eslintConfig": { 44 | "extends": "xo-space", 45 | "rules": { 46 | "eqeqeq": 0 47 | }, 48 | "env": { 49 | "jest": true, 50 | "node": true 51 | } 52 | }, 53 | "repository": "xbmc/generator-kodi-addon", 54 | "scripts": { 55 | "prepublishOnly": "nsp check", 56 | "pretest": "eslint . --fix", 57 | "test": "jest" 58 | }, 59 | "license": "Apache-2.0" 60 | } 61 | -------------------------------------------------------------------------------- /pictures/example-script.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xbmc/generator-kodi-addon/b5a70a4969c7eac339008ba7fc02b1cbdff082a4/pictures/example-script.gif --------------------------------------------------------------------------------