Initial commit

This commit is contained in:
zhongjin 2018-09-24 00:50:31 +08:00
commit d8abaf3db6
119 changed files with 4536 additions and 0 deletions

17
.gitattributes vendored Normal file
View File

@ -0,0 +1,17 @@
# Auto detect text files and perform LF normalization
* text=auto
# Custom for Visual Studio
*.cs diff=csharp
# Standard to msysgit
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain

67
.gitignore vendored Normal file
View File

@ -0,0 +1,67 @@
*.class
tasks/
.idea
.git
node_modules/
temp.txt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.ear
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
# =========================
# Operating System Files
# =========================
# OSX
# =========================
.DS_Store
.AppleDouble
.LSOverride
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Windows
# =========================
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk

10
.npmignore Normal file
View File

@ -0,0 +1,10 @@
Gruntfile.js
tasks
node_modules
.idea
.git
/node_modules
test
.travis.yml
temp.txt
appveyor.yml

23
.travis.yml Normal file
View File

@ -0,0 +1,23 @@
os:
- linux
- osx
language: node_js
node_js:
- '4'
- '6'
- '8'
- '10'
before_script:
- export NPMVERSION=$(echo "$($(which npm) -v)"|cut -c1)
- 'if [[ $NPMVERSION == 5 ]]; then npm install -g npm@5; fi'
- npm -v
- npm install winston@2.3.1
- 'npm install https://github.com/yunkong2/yunkong2.js-controller/tarball/master --production'
env:
- CXX=g++-4.8
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8

160
Gruntfile.js Normal file
View File

@ -0,0 +1,160 @@
// To use this file in WebStorm, right click on the file name in the Project Panel (normally left) and select "Open Grunt Console"
/** @namespace __dirname */
/* jshint -W097 */// jshint strict:false
/*jslint node: true */
"use strict";
function getAppName() {
var parts = __dirname.replace(/\\/g, '/').split('/');
return parts[parts.length - 1].split('.')[0].toLowerCase();
}
module.exports = function (grunt) {
var srcDir = __dirname + '/';
var pkg = grunt.file.readJSON('package.json');
var iopackage = grunt.file.readJSON('io-package.json');
var version = (pkg && pkg.version) ? pkg.version : iopackage.common.version;
var appName = getAppName();
// Project configuration.
grunt.initConfig({
pkg: pkg,
replace: {
core: {
options: {
patterns: [
{
match: /var version = *'[\.0-9]*';/g,
replacement: "var version = '" + version + "';"
},
{
match: /"version"\: *"[\.0-9]*",/g,
replacement: '"version": "' + version + '",'
}
]
},
files: [
{
expand: true,
flatten: true,
src: [
srcDir + 'controller.js',
srcDir + 'package.json',
srcDir + 'io-package.json'
],
dest: srcDir
}
]
},
name: {
options: {
patterns: [
{
match: /yunkong2/gi,
replacement: appName
}
]
},
files: [
{
expand: true,
flatten: true,
src: [
srcDir + '*',
srcDir + '.travis.yml'
],
dest: srcDir
}
]
}
},
// Javascript code styler
jscs: require(__dirname + '/tasks/jscs.js'),
// Lint
jshint: require(__dirname + '/tasks/jshint.js'),
http: {
get_hjscs: {
options: {
url: 'https://raw.githubusercontent.com/' + appName + '/' + appName + '.js-controller/master/tasks/jscs.js'
},
dest: 'tasks/jscs.js'
},
get_jshint: {
options: {
url: 'https://raw.githubusercontent.com/' + appName + '/' + appName + '.js-controller/master/tasks/jshint.js'
},
dest: 'tasks/jshint.js'
},
get_gruntfile: {
options: {
url: 'https://raw.githubusercontent.com/' + appName + '/' + appName + '.build/master/adapters/Gruntfile.js'
},
dest: 'Gruntfile.js'
},
get_utilsfile: {
options: {
url: 'https://raw.githubusercontent.com/' + appName + '/' + appName + '.build/master/adapters/utils.js'
},
dest: 'lib/utils.js'
},
get_jscsRules: {
options: {
url: 'https://raw.githubusercontent.com/' + appName + '/' + appName + '.js-controller/master/tasks/jscsRules.js'
},
dest: 'tasks/jscsRules.js'
}
}
});
grunt.registerTask('updateReadme', function () {
var readme = grunt.file.read('README.md');
var pos = readme.indexOf('## Changelog\n');
if (pos != -1) {
var readmeStart = readme.substring(0, pos + '## Changelog\n'.length);
var readmeEnd = readme.substring(pos + '## Changelog\n'.length);
if (readme.indexOf(version) == -1) {
var timestamp = new Date();
var date = timestamp.getFullYear() + '-' +
('0' + (timestamp.getMonth() + 1).toString(10)).slice(-2) + '-' +
('0' + (timestamp.getDate()).toString(10)).slice(-2);
var news = "";
if (iopackage.common.whatsNew) {
for (var i = 0; i < iopackage.common.whatsNew.length; i++) {
if (typeof iopackage.common.whatsNew[i] == 'string') {
news += '* ' + iopackage.common.whatsNew[i] + '\n';
} else {
news += '* ' + iopackage.common.whatsNew[i].en + '\n';
}
}
}
grunt.file.write('README.md', readmeStart + '### ' + version + ' (' + date + ')\n' + (news ? news + '\n\n' : '\n') + readmeEnd);
}
}
});
grunt.loadNpmTasks('grunt-replace');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-jscs');
grunt.loadNpmTasks('grunt-http');
grunt.registerTask('default', [
'http',
'replace:core',
'updateReadme',
'jshint',
'jscs'
]);
grunt.registerTask('p', [
'replace:core',
'updateReadme'
]);
grunt.registerTask('rename', [
'replace:name'
]);
};

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016, instalator
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

192
README.md Normal file
View File

@ -0,0 +1,192 @@
![Logo](admin/kodi.png)
[![NPM version](https://img.shields.io/npm/v/yunkong2.kodi.svg)](https://www.npmjs.com/package/yunkong2.kodi)
[![Downloads](https://img.shields.io/npm/dm/yunkong2.kodi.svg)](https://www.npmjs.com/package/yunkong2.kodi)
[![Tests](http://img.shields.io/travis/instalator/yunkong2.kodi/master.svg)](https://travis-ci.org/instalator/yunkong2.kodi)
[![NPM](https://nodei.co/npm/yunkong2.kodi.png?downloads=true)](https://nodei.co/npm/yunkong2.kodi/)
# Kodi's JSON-RPC API for yunkong2
You can find kodi's official documentation of the JSON-RCP API [here](http://kodi.wiki/view/JSON-RPC_API) and the full list of available commands (for protocol version 6) [here](http://kodi.wiki/view/JSON-RPC_API/v6).
***Note: This adapter requires Node 0.12+ (so 0.10 not supported)***
## Конфигурация KODI
Remote control enable.
![Remote control enable.] (admin/remote.jpg)
JSON-RPC API использует **по умолчанию порт 9090**, для того чтобы его изменить необходимо внести изменения в файл [advancedsettings.xml](http://kodi.wiki/view/AdvancedSettings.xml)
_Note: The advancedsettings.xml file does not exist by default. You have to create it first!_
```xml
<jsonrpc>
<compactoutput>true</compactoutput>
<tcpport>9999</tcpport>
</jsonrpc>
```
![http enable.](admin/web.jpg)
## Конфигурация драйвера
В найстройках драйвера указывается IP адрес KODI и порт для JSON-RPC API (по умолчанию 9090).
## Using
### ShowNotif:
Один важный момент, если используется заголовок сообщения, то он должен всегда находится перед самим текстом сообщения (Внимание;Протечка воды), расположение остальных параметров не критично.
**Image:**
Уровень сообщения
* 'info' - 0 (default),
* 'warning' - 1,
* 'error' - 2.
**displaytime:**
Время отображения сообщения в милисекундах, минимум 1500 макс 30000 мс.
**Пример:**
* 1;Внимание;Протечка воды;15000
* Внимание;Протечка воды;2;10000
* Внимание;Протечка воды
* Протечка воды
Так же сообщения можно отправлять из драйвера javascript:
```js
sendTo("kodi.0", {
message: 'Возможно протечка воды ', //Текст сообщения
title: 'ВНИМАНИЕ!!!', //Заголовок сообщения
image: 'https://raw.githubusercontent.com/instalator/yunkong2.kodi/master/admin/kodi.png', //Ссылка на иконку
delay: 7000 //Время отображения сообщения милисекундах (минимум 1500 макс 30000 мс)
});
```
### SwitchPVR:
Переключение PVR IPTV каналов по названию канала в плейлисте.
**Пример:**
ТВ канал - Discovery Science найдет как по полному наименованию так и по discover,
### Youtube:
Для открытия видео с сайта youtube достаточно записать код видео в данный статус. Начиная с версии 0.1.5 и выше можно вставлять прямую ссылку на видео, а также код или полную ссылку на плейлист.
Например: Для открытия этого [видео](https://www.youtube.com/watch?v=Bvmxr24D4TA), необходимо установить в статус - Bvmxr24D4TA
### Open:
Сюда записывается ссылка на медиконтент в сети интернет либо путь до локального медиа файла.
После записи значения начнется воспроизведение на проигрователе KODI.
### Position:
Текущая позиция в плейлисте, так же в этот статус можно записать необходимую позицую и KODI тут же перейдет к воспроизведению этой позиции.
### Seek:
Текущее значение позиции воспроизведения в процентах от 0 до 100.
### Repeat:
Повтор воспроизведения, принимает следующие значения:
* off - повтор воспроизведения отключен
* on - повтор воспроизведения текущего трека
* all - повтор всего плейлиста
### Shuffle:
Перемешивание списка треков в плейлисте для случайного воспроизведения.
Принимает значения true и false
### Play:
Старт воспроизведения (true, false)
### Speed:
Скорость воспроизведения. Фиксированные значения -32, -16, -8, -4, -2, -1, 0, 1, 2, 4, 8, 16, 32, а также increment и decrement
### Directory:
Сюда записывается путь до папки или диска, в ответ в этот статус записывается список каталогов указанной папки или диска.
### ActivateWindow:
Активизирует в проигрывателе окно. Поддерживает следующий список:
```
"home", "programs", "pictures", "filemanager", "files", "settings", "music", "video", "videos", "tv", "pvr", "pvrguideinfo", "pvrrecordinginfo", "pvrtimersetting", "pvrgroupmanager", "pvrchannelmanager", "pvrchannelmanager", "pvrguidesearch", "pvrchannelscan", "pvrupdateprogress", "pvrosdchannels", "pvrosdguide", "pvrosddirector", "pvrosdcutter", "pvrosdteletext", "systeminfo", "testpattern", "screencalibration", "guicalibration", "picturessettings", "programssettings", "weathersettings", "musicsettings", "systemsettings", "videossettings", "networksettings", "servicesettings", "appearancesettings", "pvrsettings", "tvsettings", "scripts", "videofiles", "videolibrary", "videoplaylist", "loginscreen", "profiles", "skinsettings", "addonbrowser", "yesnodialog", "progressdialog", "virtualkeyboard", "volumebar", "submenu", "favourites", "contextmenu", "infodialog", "numericinput", "gamepadinput", "shutdownmenu", "mutebug", "playercontrols", "seekbar", "musicosd", "addonsettings", "visualisationsettings", "visualisationpresetlist", "osdvideosettings", "osdaudiosettings", "videobookmarks", "filebrowser", "networksetup", "mediasource", "profilesettings", "locksettings", "contentsettings", "songinformation", "smartplaylisteditor", "smartplaylistrule", "busydialog", "pictureinfo", "accesspoints", "fullscreeninfo", "karaokeselector", "karaokelargeselector", "sliderdialog", "addoninformation", "musicplaylist", "musicfiles", "musiclibrary", "musicplaylisteditor", "teletext", "selectdialog", "musicinformation", "okdialog", "movieinformation", "textviewer", "fullscreenvideo", "fullscreenlivetv", "visualisation", "slideshow", "filestackingdialog", "karaoke", "weather", "screensaver", "videoosd", "videomenu", "videotimeseek", "musicoverlay", "videooverlay", "startwindow", "startup", "peripherals", "peripheralsettings", "extendedprogressdialog", "mediafilter".
```
### ExecuteAction:
Можно выполнить одно из следующих действий:
```
"left", "right", "up", "down", "pageup", "pagedown", "select", "highlight", "parentdir", "parentfolder", "back", "previousmenu", "info", "pause", "stop", "skipnext", "skipprevious", "fullscreen", "aspectratio", "stepforward", "stepback", "bigstepforward", "bigstepback", "osd", "showsubtitles", "nextsubtitle", "codecinfo", "nextpicture", "previouspicture", "zoomout", "zoomin", "playlist", "queue", "zoomnormal", "zoomlevel1", "zoomlevel2", "zoomlevel3", "zoomlevel4", "zoomlevel5", "zoomlevel6", "zoomlevel7", "zoomlevel8", "zoomlevel9", "nextcalibration", "resetcalibration", "analogmove", "rotate", "rotateccw", "close", "subtitledelayminus", "subtitledelay", "subtitledelayplus", "audiodelayminus", "audiodelay", "audiodelayplus", "subtitleshiftup", "subtitleshiftdown", "subtitlealign", "audionextlanguage", "verticalshiftup", "verticalshiftdown", "nextresolution", "audiotoggledigital", "number0", "number1", "number2", "number3", "number4", "number5", "number6", "number7", "number8", "number9", "osdleft", "osdright", "osdup", "osddown", "osdselect", "osdvalueplus", "osdvalueminus", "smallstepback", "fastforward", "rewind", "play", "playpause", "delete", "copy", "move", "mplayerosd", "hidesubmenu", "screenshot", "rename", "togglewatched", "scanitem", "reloadkeymaps", "volumeup", "volumedown", "mute", "backspace", "scrollup", "scrolldown", "analogfastforward", "analogrewind", "moveitemup", "moveitemdown", "contextmenu", "shift", "symbols", "cursorleft", "cursorright", "showtime", "analogseekforward", "analogseekback", "showpreset", "presetlist", "nextpreset", "previouspreset", "lockpreset", "randompreset", "increasevisrating", "decreasevisrating", "showvideomenu", "enter", "increaserating", "decreaserating", "togglefullscreen", "nextscene", "previousscene", "nextletter", "prevletter", "jumpsms2", "jumpsms3", "jumpsms4", "jumpsms5", "jumpsms6", "jumpsms7", "jumpsms8", "jumpsms9", "filter", "filterclear", "filtersms2", "filtersms3", "filtersms4", "filtersms5", "filtersms6", "filtersms7", "filtersms8", "filtersms9", "firstpage", "lastpage", "guiprofile", "red", "green", "yellow", "blue", "increasepar", "decreasepar", "volampup", "volampdown", "channelup", "channeldown", "previouschannelgroup", "nextchannelgroup", "leftclick", "rightclick", "middleclick", "doubleclick", "wheelup", "wheeldown", "mousedrag", "mousemove", "noop".
```
### System:
- EjectOpticalDrive - Извлекает или закрывает дисковод оптических дисков (если имеется)
- Hibernate - включение спящего режима
- Reboot - перезагрузка системы
- Shutdown - выключает систему
- Suspend - приостанавливает Kodi
## Changelog
#### 1.0.0 (2017-11-13)
* (instalator) up to stable
#### 0.2.4 (2017-10-16)
* (instalator) fix error
#### 0.2.3 (2017-08-15)
* (instalator) fix error switchPVR
* (instalator) Added description "System" to readme
#### 0.2.2 (2017-08-14)
* (instalator) added object - System (EjectOpticalDrive, Hibernate, Reboot, Shutdown, Suspend)
* (instalator) fix playlist widget
#### 0.2.0 (2017-01-07)
* (instalator) added object - state
* (instalator) change repeat to bool
#### 0.1.9 (2017-01-05)
* (instalator) change for vis-players
#### 0.1.8 (2016-12-25)
* (instalator) change open youtube playlist
#### 0.1.6 (2016-12-23)
* (instalator) added Tests
* (instalator) added open youtube playlist and open youtube link
#### 0.1.4 (2016-07-05)
* (instalator) fix error for Open
* (instalator) fix method number
#### 0.1.3
* (bluefox) fix error milti instance
#### 0.1.2 (2016-07-05)
* (instalator) change pvr switch - add stop
* (instalator) change dependencies
* (instalator) fix change play/stop state
#### 0.1.1 (2016-05-30)
* (instalator) change admin setting
* (instalator) Fix error 'playerid' of undefined
#### 0.1.0 (2016-05-22)
* (instalator) beta
#### 0.0.6 (2016-05-08)
* (bluefox) fixed crash when the driver turned on the KODI
* (bluefox) make adapter disabled by default, because no IP set
* (instalator) Thumbnail widget
* (instalator) added GetDirectory, GetVideoLibrary
* (instalator) added Scan & Clean Library
#### 0.0.5 (2016-05-04)
* (instalator) change creating object
* (instalator) added info.connection state
#### 0.0.4 (2016-05-03)
* (instalator) fix error
* (instalator) added VIS widgets
#### 0.0.3 (2016-05-01)
* (instalator) fix error
* (instalator) added send message from JS
#### 0.0.2 (2016-04-24)
* (instalator) remote player
* (instalator) ShowNotification
* (instalator) info playing
* (instalator) GetPVRChannel
#### 0.0.1
* (instalator) initial (17.04.2016)

69
admin/index.html Normal file
View File

@ -0,0 +1,69 @@
<html>
<link rel="stylesheet" type="text/css" href="../../lib/css/themes/jquery-ui/redmond/jquery-ui.min.css"/>
<script type="text/javascript" src="../../lib/js/jquery-1.11.1.min.js"></script>
<script type="text/javascript" src="../../socket.io/socket.io.js"></script>
<script type="text/javascript" src="../../lib/js/jquery-ui-1.10.3.full.min.js"></script>
<link rel="stylesheet" type="text/css" href="../../css/adapter.css"/>
<script type="text/javascript" src="../../js/translate.js"></script>
<script type="text/javascript" src="../../js/adapter-settings.js"></script>
<script type="text/javascript">
systemDictionary = {
"Kodi adapter settings":{"de": "Kodi adapter settings", "ru": "Настройки драйвера Kodi"},
"Kodi webserver settings":{"de": "Kodi webserver settings", "ru": "Настройки вебсервера Kodi"},
"Login:": {"en": "Login:", "de": "Login:", "ru": "Логин:"},
"Password:": {"en": "Password:", "de": "Password:", "ru": "Пароль:"},
"IP:": {"en": "IP Adress:", "de": "IP Adress:", "ru": "IP Adress KODI:"},
"PortRPC:": {"en": "Port jsonrpc", "de": "Port jsonrpc", "ru": "Порт jsonrpc:"},
"PortWeb:": {"en": "Port Webserver KODI", "de": "Port Webserver KODI", "ru": "Port Webserver KODI:"},
"default:": {"en": "default 9090", "de": "default 9090", "ru": "по умолчанию 9090"},
"on save adapter restarts with new config immediately": {
"de": "Beim Speichern von Einstellungen der Adapter wird sofort neu gestartet.",
"ru": "Сразу после сохранения настроек драйвер перезапуститься с новыми значениями"
}
};
function load(settings, onChange) {
for (var key in settings) {
if ($('#' + key + '.value').attr('type') == 'checkbox') {
$('#' + key + '.value').prop('checked', settings[key]).change(function () {
onChange();
});
} else {
$('#' + key + '.value').val(settings[key]).change(function () {
onChange();
}).keyup(function () {
$(this).trigger('change');
});
}
}
onChange(false);
}
function save(callback) {
var obj = {};
$('.value').each(function () {
var $this = $(this);
obj[$this.attr('id')] = $this.val();
});
callback(obj);
}
</script>
<div id="adapter-container">
<table><tr>
<td><img src="kodi.png"/></td>
</tr></table>
<h4 class="translate">Kodi adapter settings</h4>
<table>
<tr><td class="translate">IP:</td><td> <input class="value" id="ip"/></td></tr>
<tr><td class="translate">PortRPC:</td><td> <input class="value" id="port"/><td class="translate">default:</td></tr>
<tr><td colspan='2'><h4 class="translate">Kodi webserver settings</h4></td></tr>
<tr><td class="translate">PortWeb:</td><td> <input class="value" id="portweb"/></td> </tr>
<tr><td class="translate">Login:</td><td> <input class="value" id="login"/></td></tr>
<tr><td class="translate">Password:</td><td> <input type="password" class="value" id="password"/></td></tr>
</table>
<p class="translate">on save adapter restarts with new config immediately</p>
</div>
</html>

BIN
admin/kodi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
admin/remote.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
admin/web.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

25
appveyor.yml Normal file
View File

@ -0,0 +1,25 @@
version: 'test-{build}'
environment:
matrix:
- nodejs_version: '4'
- nodejs_version: '6'
- nodejs_version: '8'
- nodejs_version: '10'
platform:
- x86
- x64
clone_folder: 'c:\projects\%APPVEYOR_PROJECT_NAME%'
install:
- ps: 'Install-Product node $env:nodejs_version $env:platform'
- ps: '$NpmVersion = (npm -v).Substring(0,1)'
- ps: 'if($NpmVersion -eq 5) { npm install -g npm@5 }'
- ps: npm --version
- npm install
- npm install winston@2.3.1
- 'npm install https://github.com/yunkong2/yunkong2.js-controller/tarball/master --production'
test_script:
- echo %cd%
- node --version
- npm --version
- npm test
build: 'off'

1314
io-package.json Normal file

File diff suppressed because it is too large Load Diff

837
kodi.js Normal file
View File

@ -0,0 +1,837 @@
"use strict";
var kodi = require('kodi-ws');
var utils = require(__dirname + '/lib/utils');
var adapter = utils.Adapter('kodi');
//var querystring = require('querystring');
var object = {};
var connection = null;
var player_id = null;
var player_type = null;
var channel = false;
var canseek = false;
var playlist_id = 0;
var mem = null;
var mem_pos = null;
var mem_time = null;
var timer;
//TODO Изменить виджеты Коди под новый формат
adapter.on('unload', function (callback){
try {
adapter.log.info('cleaned everything up...');
callback();
} catch (e) {
callback();
}
});
adapter.on('objectChange', function (id, obj){
// Warning, obj can be null if it was deleted
adapter.log.info('objectChange ' + id + ' ' + JSON.stringify(obj));
});
adapter.on('message', function (obj){
if (typeof obj === 'object' && obj.message){
if (obj.command === 'send'){
adapter.log.debug('send command ' + JSON.stringify(obj));
var _obj = obj.message;
var param = {'title': '', 'message': '', 'image': 'info', 'displaytime': 5000};
if (typeof _obj.message !== "object"){
param.message = _obj.message;
}
param.title = _obj.title || '';
param.image = _obj.image || 'info';
param.displaytime = _obj.delay || 5000;
sendCommand('GUI.ShowNotification', param);
if (obj.callback){
adapter.sendTo(obj.from, obj.command, 'Message received', obj.callback);
}
}
}
});
adapter.on('stateChange', function (id, state){
// adapter.log.error('stateChange ' + id + ' ' + JSON.stringify(state));
if (id == adapter.namespace + '.playing_time_total' && state.val !== mem_time){
if (channel){
mem_time = state.val;
GetCurrentItem();
}
}
if (id == adapter.namespace + '.currentplay' && state.val !== mem){
mem = state.val;
GetCurrentItem();
setTimeout(function (){
GetPlayList();
}, 1000);
}
if (id == adapter.namespace + '.position' && state.val !== mem_pos){
mem_pos = state.val;
GetCurrentItem();
setTimeout(function (){
GetPlayList();
}, 1000);
}
if (state && !state.ack){
adapter.log.info('stateChange ' + id + ' ' + JSON.stringify(state));
var param = state.val;
var ids = id.split(".");
var method = ids[ids.length - 2].toString();
if (isNumeric(method)){
method = null;
}
ids = ids[ids.length - 1].toString();
ConstructorCmd(method, ids, param);
}
});
function ConstructorCmd(method, ids, param){
adapter.log.debug('ConstructorCmd ' + method + ' - ' + ids + ' = ' + param);
if (method === 'input'){
method = 'Input.' + ids;
param = [];
} else if (method === 'system'){
method = 'System.' + ids;
param = [];
} else {
switch (ids) {
case "SwitchPVR":
method = null;
SwitchPVR(param, function (res){
if(player_id){
sendCommand('Player.Stop', {'playerid': player_id}, function (){
sendCommand('Player.Open', res);
setTimeout(function (){
sendCommand('GUI.SetFullscreen', {"fullscreen": true});
}, 5000);
});
} else {
sendCommand('Player.Open', res);
setTimeout(function (){
sendCommand('GUI.SetFullscreen', {"fullscreen": true});
}, 5000);
}
});
break;
case "ShowNotif":
ShowNotification(param, function (res){
method = 'GUI.ShowNotification';
param = res;
});
break;
case "zoom":
if (param >= 0 && param <= 10){
method = 'Player.Zoom'; //
param = {"playerid": player_id, "zoom": parseInt(param)};
}
break;
case "setsubtitle":
method = 'Player.SetSubtitle'; //"previous", "next", "off", "on
param = {"playerid": player_id, "subtitle": param};
break;
case "seek":
if (param >= 0 && param <= 100 && canseek){
method = 'Player.Seek'; //int 0-100
param = {"playerid": player_id, "value": parseInt(param)}
}
break;
case "volume":
if (param >= 0 && param <= 100){
method = 'Application.SetVolume'; //int 0-100
param = parseInt(param);
}
break;
case "mute":
method = 'Application.SetMute'; //bool
param = bool(param);
break;
case "repeat":
method = 'Player.SetRepeat'; //off, on, all
param = bool(param);
if (param){
param = 'all';
} else {
param = 'off';
}
param = {'playerid': player_id, "repeat": param};
break;
case "shuffle":
method = 'Player.SetShuffle'; //bool
param = {'playerid': player_id, "shuffle": bool(param)};
break;
case "play":
param = bool(param);
if (param){
method = 'Input.ExecuteAction';
param = 'play';
} else {
method = 'Player.SetSpeed';
param = {'playerid': player_id, 'speed': 0};
}
break;
case "playid":
method = null;
if (player_id !== 'undefined'){
method = 'Player.GoTo';
} else {
sendCommand('Input.ExecuteAction', 'play', function (){ //TODO
sendCommand('Player.GoTo', {"playerid": player_id, "to": param}, function (){
});
});
}
param = {"playerid": player_id, "to": param};
break;
case "next":
method = 'Input.ExecuteAction';
param = 'skipnext';
break;
case "previous":
method = 'Input.ExecuteAction';
param = 'skipprevious';
break;
case "pause":
method = 'Player.PlayPause';
param = {'playerid': player_id, "play": "toggle"};
break;
case "stop":
method = 'Player.Stop';
param = {'playerid': player_id};
break;
case "clear":
method = 'Playlist.Clear';
param = {'playlistid': playlist_id};
adapter.setState('playlist', {val: '[]', ack: true});
break;
case "add":
var type;
method = null;
param = param.toString();
playlist_id = 0;
type = {'playlistid': playlist_id, 'item': {'file': param}};
if (param.slice(-1) === '\\' || param.slice(-1) === '/'){
type = {'playlistid': playlist_id, 'item': {'directory': param}};
}
sendCommand('Playlist.Add', type, function (){
sendCommand('Player.Open', {'item': {'playlistid': playlist_id, 'position': 0}}, function (){
sendCommand('GUI.SetFullscreen', {"fullscreen": true});
});
});
break;
case "youtube":
method = null;
if (param){
if (~param.indexOf('http')){
/*param = param.replace('&', '?').replace('#', '?');
param = querystring.parse(param, '?', '=');*/
param = param.toString().split('=');
if (param.length > 2){
param = param[param.length - 1];
param = {'item': {'file': 'plugin://plugin.video.youtube/?path=/root/video&action=play_all&playlist=' + param.toString()}};
} else if (param.length === 2){
param = param[1];
param = {'item': {'file': 'plugin://plugin.video.youtube/?path=/root/video&action=play_video&videoid=' + param.toString()}};
}
} else {
if (param.toString().length > 12){
param = {'item': {'file': 'plugin://plugin.video.youtube/?path=/root/video&action=play_all&playlist=' + param.toString()}};
} else {
param = {'item': {'file': 'plugin://plugin.video.youtube/?path=/root/video&action=play_video&videoid=' + param.toString()}};
}
}
}
sendCommand('Player.Open', param, function (){
sendCommand('Input.ExecuteAction', { "action": "select" }, function (){
sendCommand('Player.Open', {'item': {'playlistid': 1, 'position': 0}}, function (){
sendCommand('GUI.SetFullscreen', {"fullscreen": true});
});
});
});
break;
case "ActivateWindow":
method = 'GUI.ActivateWindow';
param = {"window": param};
break;
case "ExecuteAction":
method = 'Input.ExecuteAction';
param = { "action": param };
break;
case "open":
var type2;
method = null;
param = param.toString();
playlist_id = 0;
type2 = {'playlistid': playlist_id, 'item': {'file': param}};
if (param.slice(-1) === '\\'){
type2 = {'playlistid': playlist_id, 'item': {'directory': param}};
}
sendCommand('Playlist.Clear', {'playlistid': playlist_id}, function (){
sendCommand('Playlist.Add', type2, function (){
sendCommand('Player.Open', {'item': {'playlistid': playlist_id, 'position': 0}}, function (){
sendCommand('GUI.SetFullscreen', {"fullscreen": true});
});
});
});
break;
case "speed":
if (~[-32, -16, -8, -4, -2, -1, 0, 1, 2, 4, 8, 16, 32].indexOf(parseInt(param))){
method = 'Player.SetSpeed';
param = {'playerid': player_id, 'speed': parseInt(param)};
} else if (param === 'increment' || param === 'decrement'){
method = 'Player.SetSpeed';
param = {'playerid': player_id, 'speed': param};
}
break;
case "Directory":
method = null;
param = param.toString().replace("\\", "\\\\");
GetDirectory(param);
break;
case "ScanVideoLibrary":
method = 'VideoLibrary.Scan';
break;
case "ScanAudioLibrary":
method = 'AudioLibrary.Scan';
break;
case "CleanVideoLibrary":
method = 'VideoLibrary.Clean';
break;
case "CleanAudioLibrary":
method = 'AudioLibrary.Clean';
break;
default:
}
}
//adapter.log.error('stateChange ' + method + ' - ' + ids + ' = ' + JSON.stringify(param));
sendCommand(method, param);
}
function sendCommand(method, param, callback){
if (method){
getConnection(function (err, _connection){
if (_connection){
adapter.log.info('sending in KODI: ' + method + ' - ' + JSON.stringify(param));
_connection.run(method, param).then(function (result){
adapter.log.debug('response from KODI: ' + JSON.stringify(result));
if (callback) callback();
}, function (e){
ErrProcessing(e);
}).catch(function (e){
ErrProcessing(e);
})
}
});
} else {
adapter.log.debug('It does not specify commands or invalid value!');
}
}
adapter.on('ready', function (){
main();
});
function connect(){
adapter.setState('info.connection', false, true);
adapter.log.debug('KODI connecting to: ' + adapter.config.ip + ':' + adapter.config.port);
getConnection(function (err, _connection){
if (_connection){
GetNameVersion();
GetPlayerId();
GetVolume();
GetChannels();
GetVideoLibrary();
setTimeout(function (){
GetSources();
}, 10000);
connection_emit();
}
});
}
function connection_emit(){
connection.notification('Player.OnPlay', function(res) {
adapter.setState('state', {val: 'play', ack: true});
});
connection.notification('Player.OnPause', function(res) {
adapter.setState('state', {val: 'pause', ack: true});
});
connection.notification('Player.OnStop', function(res) {
adapter.setState('state', {val: 'stop', ack: true});
});
connection.notification('Input.OnInputRequested', function(res) {
//adapter.log.error('OnInputRequested: ' + JSON.stringify(res));
//{"data":{"title":"Строка поиска","type":"keyboard","value":""},"sender":"xbmc"}
//adapter.setState('OnInputRequested', {val: true, ack: true});
});
connection.notification('Playlist.OnClear', function(res) {
adapter.setState('playlist', {val: '[]', ack: true});
});
}
function GetVolume(){
if (connection){
connection.run('Application.GetProperties', {'properties': ['volume', 'muted']
}).then(function (res){
adapter.log.debug('GetVolume: ' + JSON.stringify(res));
adapter.setState('mute', {val: res.muted, ack: true});
adapter.setState('volume', {val: res.volume, ack: true});
connection.notification('Application.OnVolumeChanged', function(res) {
adapter.log.debug('OnVolumeChanged: ' + JSON.stringify(res));
adapter.setState('mute', {val: res.data.muted, ack: true});
adapter.setState('volume', {val: res.data.volume, ack: true});
});
}, function (e){
ErrProcessing(e);
}).catch(function (e){
ErrProcessing(e);
})
}
}
function main(){
/***/
//adapter.setState('Directory', false, true);
adapter.subscribeStates('*');
connect();
}
function GetSources(root){
var obj = {
'video':[],
'music':[],
'pictures':[],
'files':[],
'programs':[]
};
var count = 0;
if (connection){
Object.keys(obj).forEach(function(key) {
connection.run('Files.GetSources', {"media": key}).then(function (res){
//adapter.log.debug('GetSources: ' + JSON.stringify(res));
if (res.limits.total > 0){
for (var i = 0; i < res.limits.total; i++) {
obj[key][i] = res.sources[i];
//adapter.log.debug('GetSources: ' + JSON.stringify(obj));
}
}
count++;
if (count === 5){
adapter.log.debug('GetSources: ' + JSON.stringify(obj));
adapter.setState('Sources', {val: JSON.stringify(obj), ack: true});
filemanager(root, obj);
}
}, function (e){
ErrProcessing(e);
}).catch(function (e){
ErrProcessing(e);
})
});
}
}
function filemanager(root, obj){
var browser = {};
var files = [];
for (var key in obj){
if (obj.hasOwnProperty(key)){
if (obj[key].length > 0){
for (var i = 0; i < obj[key].length; i++) {
var o = {};
o.file = obj[key][i].file;
o.filetype = 'directory';
files.push(o);
}
}
}
}
browser.files = files;
adapter.setState('Directory', {val: JSON.stringify(browser), ack: true});
}
function GetDirectory(path){
adapter.log.debug('GetDirectory path: ' + JSON.stringify(path));
if (path !== '/'){
if (connection){
connection.run('Files.GetDirectory', {
"directory": path,
"media": "files",
"properties": ["title", "thumbnail", "fanart", "rating", "genre", "artist", "track", "season", "episode", "year", "duration", "album", "showtitle", "playcount", "file", "mimetype", "size", "lastmodified", "resume"],
"sort": {"method": "none", "order": "ascending"}
}).then(function (res){
adapter.log.debug('GetDirectory: ' + JSON.stringify(res));
adapter.setState('Directory', {val: JSON.stringify(res), ack: true});
}, function (e){
ErrProcessing(e);
}).catch(function (e){
ErrProcessing(e);
})
}
} else {
GetSources(true);
}
}
function GetVideoLibrary(){
if (connection){
connection.run('VideoLibrary.GetMovies', {
"properties": ["genre", "director", "trailer", "tagline", "plot", "plotoutline", "title", "originaltitle", "lastplayed", "runtime", "year", "playcount", "rating", "thumbnail", "file"],
"limits": {"start": 0},
"sort": {"method": "dateadded", "ignorearticle": true}
}).then(function (res){
adapter.log.debug('GetVideoLibrary: ' + JSON.stringify(res));
adapter.setState('VideoLibrary', {val: JSON.stringify(res), ack: true});
}, function (e){
ErrProcessing(e);
}).catch(function (e){
ErrProcessing(e);
})
}
}
function GetPlayList(){
if (connection){
connection.run('Playlist.GetItems', {
"playlistid": playlist_id,
"properties": ["title", "thumbnail", "fanart", "rating", "genre", "artist", "track", "season", "episode", "year", "duration", "album", "showtitle", "playcount", "file"]/*,"limits":{"start":0,"end":750}*/
}).then(function (res){
var plst = res.items;
adapter.log.debug('GetPlayList: ' + JSON.stringify(plst));
adapter.setState('playlist', {val: JSON.stringify(plst), ack: true});
}, function (e){
ErrProcessing(e);
}).catch(function (e){
ErrProcessing(e);
})
}
}
function GetCurrentItem(){
if (connection){
adapter.getStates('info.*', function (err, obj){
for (var state in obj) {
if (state !== adapter.namespace + '.info.connection'){
adapter.setState(state, {val: '', ack: true});
}
}
connection.run('Player.GetItem', {
"playerid": player_id,
"properties": ["album", "albumartist", "artist", "director", "episode", "fanart", "file", "genre", "plot", "rating", "season", "showtitle", "studio", "imdbnumber", "tagline", "thumbnail", "title", "track", "writer", "year", "streamdetails", "originaltitle", "cast", "playcount"]
}).then(function (res){
adapter.log.debug('GetCurrentItem: ' + JSON.stringify(res));
res = res.item;
for (var key in res) {
if (typeof res[key] == 'object'){
var obj = res[key];
if (key === 'streamdetails'){
for (var _key in obj) {
if (obj[_key].length > 0){
var _obj = obj[_key][0];
for (var __key in _obj) {
adapter.setState('info.' + _key + '_' + __key, {val: _obj[__key], ack: true});
//adapter.log.debug('GetPlayList: ' +_key+'_'+__key+' = '+ JSON.stringify(_obj[__key]) +' - '+typeof _obj[__key]);
}
} else {
adapter.setState('info.' + _key, {val: obj[_key], ack: true});
//adapter.log.debug('GetPlayList: ' +_key+' = '+ JSON.stringify(obj[_key]) +' - '+typeof obj[_key] +' length = '+obj[_key].length);
}
}
} else {
for (var id in obj) { //TODO
adapter.setState('info.' + key, {val: obj[id], ack: true});
//adapter.log.debug('GetPlayList: ' +_key+'_'+__key+' = '+ JSON.stringify(_obj[__key]) +' - '+typeof _obj[__key]);
}
}
} else {
adapter.setState('info.' + key, {val: res[key], ack: true});
//adapter.log.debug('GetPlayList: ' +key+' = '+ JSON.stringify(res[0][key]) +' - '+typeof res[0][key]);
}
//adapter.log.debug('GetPlayList: ' +key+' = '+ JSON.stringify(res[0][key]) +' - '+typeof res[0][key]);
}
}, function (e){
ErrProcessing(e);
}).catch(function (e){
ErrProcessing(e);
});
});
}
}
function GetNameVersion(){
if (connection){
var batch = connection.batch();
var GetProperties = batch.Application.GetProperties({"properties": ["name", "version"]});
var GetInfoBooleans = batch.XBMC.GetInfoBooleans({"booleans": ["System.Platform.Linux", "System.Platform.Linux.RaspberryPi", "System.Platform.Windows", "System.Platform.OSX", "System.Platform.IOS", "System.Platform.Darwin", "System.Platform.ATV2", "System.Platform.Android"]});
var GetInfoLabels = batch.XBMC.GetInfoLabels({"labels": ["System.KernelVersion", "System.BuildVersion"]});
batch.send();
Promise.all([GetProperties, GetInfoBooleans, GetInfoLabels]).then(function (res){
adapter.log.debug('GetNameVersion: ' + JSON.stringify(res[1]));
if (res[2]['System.KernelVersion'] === 'Ждите…' || res[2]['System.KernelVersion'] === 'Wait…' || res[2]['System.KernelVersion'] === 'Warten…'){
setTimeout(function (){
GetNameVersion();
}, 10000);
} else {
adapter.setState('systeminfo.name', {val: res[0].name, ack: true});
adapter.setState('systeminfo.version', {
val: res[0].version.major + '.' + res[0].version.minor,
ack: true
});
for (var key in res[1]) {
if (res[1][key] === true){
var system = key.split(".");
system = system[system.length - 1];
adapter.setState('systeminfo.system', {val: system, ack: true});
}
}
adapter.setState('systeminfo.kernel', {val: res[2]['System.KernelVersion'], ack: true});
}
}, function (e){
ErrProcessing(e);
}).catch(function (e){
ErrProcessing(e);
});
}
}
function GetChannels(){
if (connection){
var batch = connection.batch();
var alltv = batch.PVR.GetChannels({
"channelgroupid": "alltv",
"properties": ["channel", "channeltype", "hidden", "lastplayed", "locked", "thumbnail", "broadcastnow"]
});
var allradio = batch.PVR.GetChannels({
"channelgroupid": "allradio",
"properties": ["channel", "channeltype", "hidden", "lastplayed", "locked", "thumbnail", "broadcastnow"]
});
batch.send();
Promise.all([alltv, allradio]).then(function (res){
if(res){
adapter.setState('pvr.playlist_tv', {val: JSON.stringify(res[0]), ack: true});
adapter.setState('pvr.playlist_radio', {val: JSON.stringify(res[1]), ack: true});
}
}, function (e){
ErrProcessing(e);
}).catch(function (e){
ErrProcessing(e);
});
}
}
function GetPlayerProperties(){
if (connection && player_id !== undefined && player_id !== null){
var batch = connection.batch();
var Properties = batch.Player.GetProperties({
"playerid": player_id,
"properties": ["audiostreams", "canseek", "currentaudiostream", "currentsubtitle", "partymode", "playlistid", "position", "repeat", "shuffled", "speed", "subtitleenabled", "subtitles", "time", "totaltime", "type"]
});
var InfoLabels = batch.XBMC.GetInfoLabels({"labels": ["MusicPlayer.Codec", "MusicPlayer.SampleRate", "MusicPlayer.BitRate"]});
var CurrentPlay = batch.Player.GetItem({"playerid": player_id});
batch.send();
Promise.all([Properties, InfoLabels, CurrentPlay]).then(function (res){
//TODO сохранять только изменения
//pre = res[0];
adapter.log.debug('Response GetPlayerProperties ' + JSON.stringify(res));
var total = (res[0].totaltime.hours * 3600) + (res[0].totaltime.minutes * 60) + res[0].totaltime.seconds;
var cur = (res[0].time.hours * 3600) + (res[0].time.minutes * 60) + res[0].time.seconds;
playlist_id = res[0].playlistid;
adapter.setState('playing_time', {
val: time(res[0].time.hours, res[0].time.minutes, res[0].time.seconds),
ack: true
});
adapter.setState('playing_time_total', {
val: time(res[0].totaltime.hours, res[0].totaltime.minutes, res[0].totaltime.seconds),
ack: true
});
canseek = res[0].canseek;
adapter.setState('seek', {val: parseInt(cur * 100 / total), ack: true});
adapter.setState('canseek', {val: res[0].canseek, ack: true});
adapter.setState('repeat', {val: res[0].repeat, ack: true});
adapter.setState('shuffle', {val: res[0].shuffled, ack: true});
adapter.setState('speed', {val: res[0].speed, ack: true});
adapter.setState('position', {val: res[0].position, ack: true});
adapter.setState('playlistid', {val: res[0].playlistid, ack: true});
adapter.setState('partymode', {val: res[0].partymode, ack: true});
if (res[0].audiostreams.length > 0){
adapter.setState('codec', {val: res[0].audiostreams[0].codec, ack: true});
adapter.setState('bitrate', {val: res[0].audiostreams[0].bitrate, ack: true});
adapter.setState('channels', {val: res[0].audiostreams[0].channels, ack: true});
adapter.setState('language', {val: res[0].audiostreams[0].language, ack: true});
adapter.setState('audiostream', {val: res[0].audiostreams[0].name, ack: true});
} else {
adapter.setState('channels', {val: 2, ack: true});
adapter.setState('audiostream', {val: '', ack: true});
adapter.setState('language', {val: '', ack: true});
adapter.setState('codec', {val: res[1]['MusicPlayer.Codec'], ack: true});
adapter.setState('samplerate', {val: res[1]['MusicPlayer.SampleRate'], ack: true});
adapter.setState('bitrate', {val: res[1]['MusicPlayer.BitRate'], ack: true});
}
if (res[2].item.type == 'channel'){
adapter.setState('type', {val: res[2].item.type, ack: true});
channel = true;
} else {
adapter.setState('type', {val: res[0].type, ack: true});
channel = false;
}
if (res[2].item.label.toString().length < 2){
setTimeout(function (){
adapter.getState(adapter.namespace + '.info.file', function (err, state){
state = state.val.substring(state.val.lastIndexOf('/') + 1, state.val.length - 4);
adapter.setState('currentplay', {val: state, ack: true});
});
}, 1000);
} else {
adapter.setState('currentplay', {val: res[2].item.label, ack: true});
}
}, function (e){
ErrProcessing(e);
}).catch(function (e){
ErrProcessing(e);
});
}
}
function GetPlayerId(){
clearTimeout(timer);
if (connection){
connection.run('Player.GetActivePlayers').then(function (res){
adapter.log.debug('Response GetPlayerId: ' + JSON.stringify(res));
if (res.length > 0){
player_id = res[0].playerid;
player_type = res[0].type;
GetPlayerProperties();
} else {
player_id = null;
player_type = null;
}
timer = setTimeout(function (){
GetPlayerId();
}, 2000);
}, function (e){
ErrProcessing(e);
}).catch(function (e){
ErrProcessing(e);
})
} else {
connect();
}
}
function getConnection(cb){
if (connection){
cb && cb(null, connection);
return;
}
clearTimeout(timer);
kodi(adapter.config.ip, adapter.config.port).then(function (_connection){
connection = _connection;
_connection.on('error', function (err){
adapter.log.warn('Error: ' + err);
}).on('close', function (){
if (connection){
console.log('Connection closed');
if (connection.socket) connection.socket.close();
connection = null;
setTimeout(connect, 5000);
}
});
adapter.log.info('KODI connected');
adapter.setState('info.connection', true, true);
GetPlayerId();
cb && cb(null, connection);
}, function (error){
adapter.log.debug(error);
adapter.setState('info.connection', false, true);
setTimeout(connect, 5000, cb);
}).catch(function (error){
if (error.stack){
adapter.log.error(error.stack);
} else {
adapter.log.error(error);
}
adapter.setState('info.connection', false, true);
setTimeout(connect, 5000, cb);
});
}
function time(hour, min, sec){
var time = '';
hour = (parseInt(hour) < 10 ? '0' : '') + hour;
min = (parseInt(min) < 10 ? '0' : '') + min;
sec = (parseInt(sec) < 10 ? '0' : '') + sec;
if (parseInt(hour) === 0){
time = min + ':' + sec;
} else {
time = hour + ':' + min + ':' + sec;
}
return time;
}
function SwitchPVR(val, callback){
adapter.getState(adapter.namespace + '.pvr.playlist_tv', function (err, state){
if (state){
var Break = {};
val = val.toString().toLowerCase();
var obj = JSON.parse(state.val);
try {
obj.channels.forEach(function (item, i, a){
var channel = item.label.toString().toLowerCase();
var pos = channel.indexOf(val);
if (pos === 0){ //TODO
//adapter.log.debug('PVR.GetChannelsIPTV: '+item.channelid);
callback({"item": {"channelid": item.channelid}});
throw Break;
}
});
} catch (e) {
if (e !== Break) throw e;
}
}
});
}
function ShowNotification(param, callback){
var title = '';
var message = '';
var displaytime = 5000;
var img = ['info', 'warning', 'error'];
var image = 'info';
var c = (';' + param).split(';');
var flag = false;
c.forEach(function (item, i, arr){
if (!isNaN(item)){
var num = parseInt(item);
if (num >= 1500 && num <= 30000){
displaytime = num;
}
else if (num >= 0 && num <= 2){
image = img[num];
}
}
if (isNaN(arr[i]) && isNaN(arr[i + 1]) && flag === false){
if (arr[i] && arr[i + 1]){
title = arr[i].toString();
message = arr[i + 1].toString();
flag = true;
}
}
});
if (!flag){
c.forEach(function (item, i, arr){
if (isNaN(arr[i]) && arr[i]){
message = arr[i].toString();
}
});
}
callback({'title': title, 'message': message, 'image': image, 'displaytime': displaytime});
}
function bool(s){
//s = s.toString();
if (s === 1 || s === '1' || s === 'true' || s === true){
return true;
} else {
return false;
}
}
function ErrProcessing(error){
adapter.log.error(error);
connection = null;
getConnection();
}
function isNumeric(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
}

83
lib/utils.js Normal file
View File

@ -0,0 +1,83 @@
'use strict';
const fs = require('fs');
const path = require('path');
let controllerDir;
let appName;
/**
* returns application name
*
* The name of the application can be different and this function finds it out.
*
* @returns {string}
*/
function getAppName() {
const parts = __dirname.replace(/\\/g, '/').split('/');
return parts[parts.length - 2].split('.')[0];
}
/**
* looks for js-controller home folder
*
* @param {boolean} isInstall
* @returns {string}
*/
function getControllerDir(isInstall) {
// Find the js-controller location
const possibilities = [
'yunkong2.js-controller',
'yunkong2.js-controller',
];
/** @type {string} */
let controllerPath;
for (const pkg of possibilities) {
try {
const possiblePath = require.resolve(pkg);
if (fs.existsSync(possiblePath)) {
controllerPath = possiblePath;
break;
}
} catch (e) { /* not found */ }
}
if (controllerPath == null) {
if (!isInstall) {
console.log('Cannot find js-controller');
process.exit(10);
} else {
process.exit();
}
}
// we found the controller
return path.dirname(controllerPath);
}
/**
* reads controller base settings
*
* @alias getConfig
* @returns {object}
*/
function getConfig() {
let configPath;
if (fs.existsSync(
configPath = path.join(controllerDir, 'conf', appName + '.json')
)) {
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
} else if (fs.existsSync(
configPath = path.join(controllerDir, 'conf', + appName.toLowerCase() + '.json')
)) {
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
} else {
throw new Error('Cannot find ' + controllerDir + '/conf/' + appName + '.json');
}
}
appName = getAppName();
controllerDir = getControllerDir(typeof process !== 'undefined' && process.argv && process.argv.indexOf('--install') !== -1);
const adapter = require(path.join(controllerDir, 'lib/adapter.js'));
exports.controllerDir = controllerDir;
exports.getConfig = getConfig;
exports.Adapter = adapter;
exports.appName = appName;

51
package.json Normal file
View File

@ -0,0 +1,51 @@
{
"name": "yunkong2.kodi",
"version": "1.0.0",
"description": "yunkong2.kodi Adapter",
"author": {
"name": "instalator",
"email": "vvvalt@mail.ru"
},
"contributors": [
{
"name": "instalator",
"email": "vvvalt@mail.ru"
}
],
"homepage": "https://github.com/instalator/yunkong2.kodi",
"license": "MIT",
"keywords": [
"yunkong2",
"kodi xbmc",
"Smart Home",
"home automation"
],
"repository": {
"type": "git",
"url": "https://github.com/instalator/yunkong2.kodi"
},
"dependencies": {
"kodi-ws": "https://github.com/instalator/kodi-ws/tarball/master/"
},
"devDependencies": {
"grunt": "^1.0.1",
"grunt-replace": "^1.0.1",
"grunt-contrib-jshint": "^1.1.0",
"grunt-jscs": "^3.0.1",
"grunt-http": "^2.2.0",
"grunt-contrib-clean": "^1.1.0",
"grunt-contrib-compress": "^1.4.3",
"grunt-contrib-copy": "^1.0.0",
"grunt-exec": "^3.0.0",
"mocha": "^4.1.0",
"chai": "^4.1.2"
},
"main": "kodi.js",
"scripts": {
"test": "node node_modules/mocha/bin/mocha --exit"
},
"bugs": {
"url": "https://github.com/instalator/yunkong2.kodi/issues"
},
"readmeFilename": "README.md"
}

728
test/lib/setup.js Normal file
View File

@ -0,0 +1,728 @@
/* jshint -W097 */// jshint strict:false
/*jslint node: true */
// check if tmp directory exists
var fs = require('fs');
var path = require('path');
var child_process = require('child_process');
var rootDir = path.normalize(__dirname + '/../../');
var pkg = require(rootDir + 'package.json');
var debug = typeof v8debug === 'object';
pkg.main = pkg.main || 'main.js';
var adapterName = path.normalize(rootDir).replace(/\\/g, '/').split('/');
adapterName = adapterName[adapterName.length - 2];
var adapterStarted = false;
function getAppName() {
var parts = __dirname.replace(/\\/g, '/').split('/');
return parts[parts.length - 3].split('.')[0];
}
var appName = getAppName().toLowerCase();
var objects;
var states;
var pid = null;
function copyFileSync(source, target) {
var targetFile = target;
//if target is a directory a new file with the same name will be created
if (fs.existsSync(target)) {
if ( fs.lstatSync( target ).isDirectory() ) {
targetFile = path.join(target, path.basename(source));
}
}
try {
fs.writeFileSync(targetFile, fs.readFileSync(source));
}
catch (err) {
console.log("file copy error: " +source +" -> " + targetFile + " (error ignored)");
}
}
function copyFolderRecursiveSync(source, target, ignore) {
var files = [];
var base = path.basename(source);
if (base === adapterName) {
base = pkg.name;
}
//check if folder needs to be created or integrated
var targetFolder = path.join(target, base);
if (!fs.existsSync(targetFolder)) {
fs.mkdirSync(targetFolder);
}
//copy
if (fs.lstatSync(source).isDirectory()) {
files = fs.readdirSync(source);
files.forEach(function (file) {
if (ignore && ignore.indexOf(file) !== -1) {
return;
}
var curSource = path.join(source, file);
var curTarget = path.join(targetFolder, file);
if (fs.lstatSync(curSource).isDirectory()) {
// ignore grunt files
if (file.indexOf('grunt') !== -1) return;
if (file === 'chai') return;
if (file === 'mocha') return;
copyFolderRecursiveSync(curSource, targetFolder, ignore);
} else {
copyFileSync(curSource, curTarget);
}
});
}
}
if (!fs.existsSync(rootDir + 'tmp')) {
fs.mkdirSync(rootDir + 'tmp');
}
function storeOriginalFiles() {
console.log('Store original files...');
var dataDir = rootDir + 'tmp/' + appName + '-data/';
var f = fs.readFileSync(dataDir + 'objects.json');
var objects = JSON.parse(f.toString());
if (objects['system.adapter.admin.0'] && objects['system.adapter.admin.0'].common) {
objects['system.adapter.admin.0'].common.enabled = false;
}
if (objects['system.adapter.admin.1'] && objects['system.adapter.admin.1'].common) {
objects['system.adapter.admin.1'].common.enabled = false;
}
fs.writeFileSync(dataDir + 'objects.json.original', JSON.stringify(objects));
try {
f = fs.readFileSync(dataDir + 'states.json');
fs.writeFileSync(dataDir + 'states.json.original', f);
}
catch (err) {
console.log('no states.json found - ignore');
}
}
function restoreOriginalFiles() {
console.log('restoreOriginalFiles...');
var dataDir = rootDir + 'tmp/' + appName + '-data/';
var f = fs.readFileSync(dataDir + 'objects.json.original');
fs.writeFileSync(dataDir + 'objects.json', f);
try {
f = fs.readFileSync(dataDir + 'states.json.original');
fs.writeFileSync(dataDir + 'states.json', f);
}
catch (err) {
console.log('no states.json.original found - ignore');
}
}
function checkIsAdapterInstalled(cb, counter, customName) {
customName = customName || pkg.name.split('.').pop();
counter = counter || 0;
var dataDir = rootDir + 'tmp/' + appName + '-data/';
console.log('checkIsAdapterInstalled...');
try {
var f = fs.readFileSync(dataDir + 'objects.json');
var objects = JSON.parse(f.toString());
if (objects['system.adapter.' + customName + '.0']) {
console.log('checkIsAdapterInstalled: ready!');
setTimeout(function () {
if (cb) cb();
}, 100);
return;
} else {
console.warn('checkIsAdapterInstalled: still not ready');
}
} catch (err) {
}
if (counter > 20) {
console.error('checkIsAdapterInstalled: Cannot install!');
if (cb) cb('Cannot install');
} else {
console.log('checkIsAdapterInstalled: wait...');
setTimeout(function() {
checkIsAdapterInstalled(cb, counter + 1);
}, 1000);
}
}
function checkIsControllerInstalled(cb, counter) {
counter = counter || 0;
var dataDir = rootDir + 'tmp/' + appName + '-data/';
console.log('checkIsControllerInstalled...');
try {
var f = fs.readFileSync(dataDir + 'objects.json');
var objects = JSON.parse(f.toString());
if (objects['system.adapter.admin.0']) {
console.log('checkIsControllerInstalled: installed!');
setTimeout(function () {
if (cb) cb();
}, 100);
return;
}
} catch (err) {
}
if (counter > 20) {
console.log('checkIsControllerInstalled: Cannot install!');
if (cb) cb('Cannot install');
} else {
console.log('checkIsControllerInstalled: wait...');
setTimeout(function() {
checkIsControllerInstalled(cb, counter + 1);
}, 1000);
}
}
function installAdapter(customName, cb) {
if (typeof customName === 'function') {
cb = customName;
customName = null;
}
customName = customName || pkg.name.split('.').pop();
console.log('Install adapter...');
var startFile = 'node_modules/' + appName + '.js-controller/' + appName + '.js';
// make first install
if (debug) {
child_process.execSync('node ' + startFile + ' add ' + customName + ' --enabled false', {
cwd: rootDir + 'tmp',
stdio: [0, 1, 2]
});
checkIsAdapterInstalled(function (error) {
if (error) console.error(error);
console.log('Adapter installed.');
if (cb) cb();
});
} else {
// add controller
var _pid = child_process.fork(startFile, ['add', customName, '--enabled', 'false'], {
cwd: rootDir + 'tmp',
stdio: [0, 1, 2, 'ipc']
});
waitForEnd(_pid, function () {
checkIsAdapterInstalled(function (error) {
if (error) console.error(error);
console.log('Adapter installed.');
if (cb) cb();
});
});
}
}
function waitForEnd(_pid, cb) {
if (!_pid) {
cb(-1, -1);
return;
}
_pid.on('exit', function (code, signal) {
if (_pid) {
_pid = null;
cb(code, signal);
}
});
_pid.on('close', function (code, signal) {
if (_pid) {
_pid = null;
cb(code, signal);
}
});
}
function installJsController(cb) {
console.log('installJsController...');
if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller') ||
!fs.existsSync(rootDir + 'tmp/' + appName + '-data')) {
// try to detect appName.js-controller in node_modules/appName.js-controller
// travis CI installs js-controller into node_modules
if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller')) {
console.log('installJsController: no js-controller => copy it from "' + rootDir + 'node_modules/' + appName + '.js-controller"');
// copy all
// stop controller
console.log('Stop controller if running...');
var _pid;
if (debug) {
// start controller
_pid = child_process.exec('node ' + appName + '.js stop', {
cwd: rootDir + 'node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2]
});
} else {
_pid = child_process.fork(appName + '.js', ['stop'], {
cwd: rootDir + 'node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2, 'ipc']
});
}
waitForEnd(_pid, function () {
// copy all files into
if (!fs.existsSync(rootDir + 'tmp')) fs.mkdirSync(rootDir + 'tmp');
if (!fs.existsSync(rootDir + 'tmp/node_modules')) fs.mkdirSync(rootDir + 'tmp/node_modules');
if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')){
console.log('Copy js-controller...');
copyFolderRecursiveSync(rootDir + 'node_modules/' + appName + '.js-controller', rootDir + 'tmp/node_modules/');
}
console.log('Setup js-controller...');
var __pid;
if (debug) {
// start controller
_pid = child_process.exec('node ' + appName + '.js setup first --console', {
cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2]
});
} else {
__pid = child_process.fork(appName + '.js', ['setup', 'first', '--console'], {
cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2, 'ipc']
});
}
waitForEnd(__pid, function () {
checkIsControllerInstalled(function () {
// change ports for object and state DBs
var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json');
config.objects.port = 19001;
config.states.port = 19000;
fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2));
console.log('Setup finished.');
copyAdapterToController();
installAdapter(function () {
storeOriginalFiles();
if (cb) cb(true);
});
});
});
});
} else {
// check if port 9000 is free, else admin adapter will be added to running instance
var client = new require('net').Socket();
client.connect(9000, '127.0.0.1', function() {
console.error('Cannot initiate fisrt run of test, because one instance of application is running on this PC. Stop it and repeat.');
process.exit(0);
});
setTimeout(function () {
client.destroy();
if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')) {
console.log('installJsController: no js-controller => install from git');
child_process.execSync('npm install https://github.com/' + appName + '/' + appName + '.js-controller/tarball/master --prefix ./ --production', {
cwd: rootDir + 'tmp/',
stdio: [0, 1, 2]
});
} else {
console.log('Setup js-controller...');
var __pid;
if (debug) {
// start controller
child_process.exec('node ' + appName + '.js setup first', {
cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2]
});
} else {
child_process.fork(appName + '.js', ['setup', 'first'], {
cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2, 'ipc']
});
}
}
// let npm install admin and run setup
checkIsControllerInstalled(function () {
var _pid;
if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller/' + appName + '.js')) {
_pid = child_process.fork(appName + '.js', ['stop'], {
cwd: rootDir + 'node_modules/' + appName + '.js-controller',
stdio: [0, 1, 2, 'ipc']
});
}
waitForEnd(_pid, function () {
// change ports for object and state DBs
var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json');
config.objects.port = 19001;
config.states.port = 19000;
fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2));
copyAdapterToController();
installAdapter(function () {
storeOriginalFiles();
if (cb) cb(true);
});
});
});
}, 1000);
}
} else {
setTimeout(function () {
console.log('installJsController: js-controller installed');
if (cb) cb(false);
}, 0);
}
}
function copyAdapterToController() {
console.log('Copy adapter...');
// Copy adapter to tmp/node_modules/appName.adapter
copyFolderRecursiveSync(rootDir, rootDir + 'tmp/node_modules/', ['.idea', 'test', 'tmp', '.git', appName + '.js-controller']);
console.log('Adapter copied.');
}
function clearControllerLog() {
var dirPath = rootDir + 'tmp/log';
var files;
try {
if (fs.existsSync(dirPath)) {
console.log('Clear controller log...');
files = fs.readdirSync(dirPath);
} else {
console.log('Create controller log directory...');
files = [];
fs.mkdirSync(dirPath);
}
} catch(e) {
console.error('Cannot read "' + dirPath + '"');
return;
}
if (files.length > 0) {
try {
for (var i = 0; i < files.length; i++) {
var filePath = dirPath + '/' + files[i];
fs.unlinkSync(filePath);
}
console.log('Controller log cleared');
} catch (err) {
console.error('cannot clear log: ' + err);
}
}
}
function clearDB() {
var dirPath = rootDir + 'tmp/yunkong2-data/sqlite';
var files;
try {
if (fs.existsSync(dirPath)) {
console.log('Clear sqlite DB...');
files = fs.readdirSync(dirPath);
} else {
console.log('Create controller log directory...');
files = [];
fs.mkdirSync(dirPath);
}
} catch(e) {
console.error('Cannot read "' + dirPath + '"');
return;
}
if (files.length > 0) {
try {
for (var i = 0; i < files.length; i++) {
var filePath = dirPath + '/' + files[i];
fs.unlinkSync(filePath);
}
console.log('Clear sqlite DB');
} catch (err) {
console.error('cannot clear DB: ' + err);
}
}
}
function setupController(cb) {
installJsController(function (isInited) {
clearControllerLog();
clearDB();
if (!isInited) {
restoreOriginalFiles();
copyAdapterToController();
}
// read system.config object
var dataDir = rootDir + 'tmp/' + appName + '-data/';
var objs;
try {
objs = fs.readFileSync(dataDir + 'objects.json');
objs = JSON.parse(objs);
}
catch (e) {
console.log('ERROR reading/parsing system configuration. Ignore');
objs = {'system.config': {}};
}
if (!objs || !objs['system.config']) {
objs = {'system.config': {}};
}
if (cb) cb(objs['system.config']);
});
}
function startAdapter(objects, states, callback) {
if (adapterStarted) {
console.log('Adapter already started ...');
if (callback) callback(objects, states);
return;
}
adapterStarted = true;
console.log('startAdapter...');
if (fs.existsSync(rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main)) {
try {
if (debug) {
// start controller
pid = child_process.exec('node node_modules/' + pkg.name + '/' + pkg.main + ' --console silly', {
cwd: rootDir + 'tmp',
stdio: [0, 1, 2]
});
} else {
// start controller
pid = child_process.fork('node_modules/' + pkg.name + '/' + pkg.main, ['--console', 'silly'], {
cwd: rootDir + 'tmp',
stdio: [0, 1, 2, 'ipc']
});
}
} catch (error) {
console.error(JSON.stringify(error));
}
} else {
console.error('Cannot find: ' + rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main);
}
if (callback) callback(objects, states);
}
function startController(isStartAdapter, onObjectChange, onStateChange, callback) {
if (typeof isStartAdapter === 'function') {
callback = onStateChange;
onStateChange = onObjectChange;
onObjectChange = isStartAdapter;
isStartAdapter = true;
}
if (onStateChange === undefined) {
callback = onObjectChange;
onObjectChange = undefined;
}
if (pid) {
console.error('Controller is already started!');
} else {
console.log('startController...');
adapterStarted = false;
var isObjectConnected;
var isStatesConnected;
var Objects = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/objects/objectsInMemServer');
objects = new Objects({
connection: {
"type" : "file",
"host" : "127.0.0.1",
"port" : 19001,
"user" : "",
"pass" : "",
"noFileCache": false,
"connectTimeout": 2000
},
logger: {
silly: function (msg) {
console.log(msg);
},
debug: function (msg) {
console.log(msg);
},
info: function (msg) {
console.log(msg);
},
warn: function (msg) {
console.warn(msg);
},
error: function (msg) {
console.error(msg);
}
},
connected: function () {
isObjectConnected = true;
if (isStatesConnected) {
console.log('startController: started!');
if (isStartAdapter) {
startAdapter(objects, states, callback);
} else {
if (callback) {
callback(objects, states);
callback = null;
}
}
}
},
change: onObjectChange
});
// Just open in memory DB itself
var States = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/states/statesInMemServer');
states = new States({
connection: {
type: 'file',
host: '127.0.0.1',
port: 19000,
options: {
auth_pass: null,
retry_max_delay: 15000
}
},
logger: {
silly: function (msg) {
console.log(msg);
},
debug: function (msg) {
console.log(msg);
},
info: function (msg) {
console.log(msg);
},
warn: function (msg) {
console.log(msg);
},
error: function (msg) {
console.log(msg);
}
},
connected: function () {
isStatesConnected = true;
if (isObjectConnected) {
console.log('startController: started!!');
if (isStartAdapter) {
startAdapter(objects, states, callback);
} else {
if (callback) {
callback(objects, states);
callback = null;
}
}
}
},
change: onStateChange
});
}
}
function stopAdapter(cb) {
if (!pid) {
console.error('Controller is not running!');
if (cb) {
setTimeout(function () {
cb(false);
}, 0);
}
} else {
adapterStarted = false;
pid.on('exit', function (code, signal) {
if (pid) {
console.log('child process terminated due to receipt of signal ' + signal);
if (cb) cb();
pid = null;
}
});
pid.on('close', function (code, signal) {
if (pid) {
if (cb) cb();
pid = null;
}
});
pid.kill('SIGTERM');
}
}
function _stopController() {
if (objects) {
objects.destroy();
objects = null;
}
if (states) {
states.destroy();
states = null;
}
}
function stopController(cb) {
var timeout;
if (objects) {
console.log('Set system.adapter.' + pkg.name + '.0');
objects.setObject('system.adapter.' + pkg.name + '.0', {
common:{
enabled: false
}
});
}
stopAdapter(function () {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
_stopController();
if (cb) {
cb(true);
cb = null;
}
});
timeout = setTimeout(function () {
timeout = null;
console.log('child process NOT terminated');
_stopController();
if (cb) {
cb(false);
cb = null;
}
pid = null;
}, 5000);
}
// Setup the adapter
function setAdapterConfig(common, native, instance) {
var objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString());
var id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0);
if (common) objects[id].common = common;
if (native) objects[id].native = native;
fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/objects.json', JSON.stringify(objects));
}
// Read config of the adapter
function getAdapterConfig(instance) {
var objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString());
var id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0);
return objects[id];
}
if (typeof module !== undefined && module.parent) {
module.exports.getAdapterConfig = getAdapterConfig;
module.exports.setAdapterConfig = setAdapterConfig;
module.exports.startController = startController;
module.exports.stopController = stopController;
module.exports.setupController = setupController;
module.exports.stopAdapter = stopAdapter;
module.exports.startAdapter = startAdapter;
module.exports.installAdapter = installAdapter;
module.exports.appName = appName;
module.exports.adapterName = adapterName;
module.exports.adapterStarted = adapterStarted;
}

140
test/testAdapter.js Normal file
View File

@ -0,0 +1,140 @@
/* jshint -W097 */// jshint strict:false
/*jslint node: true */
var expect = require('chai').expect;
var setup = require(__dirname + '/lib/setup');
var objects = null;
var states = null;
var onStateChanged = null;
var onObjectChanged = null;
var sendToID = 1;
var adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.')+1);
function checkConnectionOfAdapter(cb, counter) {
counter = counter || 0;
console.log('Try check #' + counter);
if (counter > 30) {
if (cb) cb('Cannot check connection');
return;
}
states.getState('system.adapter.' + adapterShortName + '.0.alive', function (err, state) {
if (err) console.error(err);
if (state && state.val) {
if (cb) cb();
} else {
setTimeout(function () {
checkConnectionOfAdapter(cb, counter + 1);
}, 1000);
}
});
}
function checkValueOfState(id, value, cb, counter) {
counter = counter || 0;
if (counter > 20) {
if (cb) cb('Cannot check value Of State ' + id);
return;
}
states.getState(id, function (err, state) {
if (err) console.error(err);
if (value === null && !state) {
if (cb) cb();
} else
if (state && (value === undefined || state.val === value)) {
if (cb) cb();
} else {
setTimeout(function () {
checkValueOfState(id, value, cb, counter + 1);
}, 500);
}
});
}
function sendTo(target, command, message, callback) {
onStateChanged = function (id, state) {
if (id === 'messagebox.system.adapter.test.0') {
callback(state.message);
}
};
states.pushMessage('system.adapter.' + target, {
command: command,
message: message,
from: 'system.adapter.test.0',
callback: {
message: message,
id: sendToID++,
ack: false,
time: (new Date()).getTime()
}
});
}
describe('Test ' + adapterShortName + ' adapter', function() {
before('Test ' + adapterShortName + ' adapter: Start js-controller', function (_done) {
this.timeout(600000); // because of first install from npm
setup.setupController(function () {
var config = setup.getAdapterConfig();
// enable adapter
config.common.enabled = true;
config.common.loglevel = 'debug';
//config.native.dbtype = 'sqlite';
setup.setAdapterConfig(config.common, config.native);
setup.startController(true, function(id, obj) {}, function (id, state) {
if (onStateChanged) onStateChanged(id, state);
},
function (_objects, _states) {
objects = _objects;
states = _states;
_done();
});
});
});
/*
ENABLE THIS WHEN ADAPTER RUNS IN DEAMON MODE TO CHECK THAT IT HAS STARTED SUCCESSFULLY
*/
it('Test ' + adapterShortName + ' adapter: Check if adapter started', function (done) {
this.timeout(60000);
checkConnectionOfAdapter(function (res) {
if (res) console.log(res);
expect(res).not.to.be.equal('Cannot check connection');
objects.setObject('system.adapter.test.0', {
common: {
},
type: 'instance'
},
function () {
states.subscribeMessage('system.adapter.test.0');
done();
});
});
});
/**/
/*
PUT YOUR OWN TESTS HERE USING
it('Testname', function ( done) {
...
});
You can also use "sendTo" method to send messages to the started adapter
*/
after('Test ' + adapterShortName + ' adapter: Stop js-controller', function (done) {
this.timeout(10000);
setup.stopController(function (normalTerminated) {
console.log('Adapter normal terminated: ' + normalTerminated);
done();
});
});
});

91
test/testPackageFiles.js Normal file
View File

@ -0,0 +1,91 @@
/* jshint -W097 */
/* jshint strict:false */
/* jslint node: true */
/* jshint expr: true */
var expect = require('chai').expect;
var fs = require('fs');
describe('Test package.json and io-package.json', function() {
it('Test package files', function (done) {
console.log();
var fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json', 'utf8');
var ioPackage = JSON.parse(fileContentIOPackage);
var fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json', 'utf8');
var npmPackage = JSON.parse(fileContentNPMPackage);
expect(ioPackage).to.be.an('object');
expect(npmPackage).to.be.an('object');
expect(ioPackage.common.version, 'ERROR: Version number in io-package.json needs to exist').to.exist;
expect(npmPackage.version, 'ERROR: Version number in package.json needs to exist').to.exist;
expect(ioPackage.common.version, 'ERROR: Version numbers in package.json and io-package.json needs to match').to.be.equal(npmPackage.version);
if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) {
console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!');
console.log();
}
expect(npmPackage.author, 'ERROR: Author in package.json needs to exist').to.exist;
expect(ioPackage.common.authors, 'ERROR: Authors in io-package.json needs to exist').to.exist;
if (ioPackage.common.name.indexOf('template') !== 0) {
if (Array.isArray(ioPackage.common.authors)) {
expect(ioPackage.common.authors.length, 'ERROR: Author in io-package.json needs to be set').to.not.be.equal(0);
if (ioPackage.common.authors.length === 1) {
expect(ioPackage.common.authors[0], 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name <my@email.com>');
}
}
else {
expect(ioPackage.common.authors, 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name <my@email.com>');
}
}
else {
console.log('WARNING: Testing for set authors field in io-package skipped because template adapter');
console.log();
}
expect(fs.existsSync(__dirname + '/../README.md'), 'ERROR: README.md needs to exist! Please create one with description, detail information and changelog. English is mandatory.').to.be.true;
if (!ioPackage.common.titleLang || typeof ioPackage.common.titleLang !== 'object') {
console.log('WARNING: titleLang is not existing in io-package.json. Please add');
console.log();
}
if (
ioPackage.common.title.indexOf('yunkong2') !== -1 ||
ioPackage.common.title.indexOf('yunkong2') !== -1 ||
ioPackage.common.title.indexOf('adapter') !== -1 ||
ioPackage.common.title.indexOf('Adapter') !== -1
) {
console.log('WARNING: title contains Adapter or yunkong2. It is clear anyway, that it is adapter for yunkong2.');
console.log();
}
if (ioPackage.common.name.indexOf('vis-') !== 0) {
if (!ioPackage.common.materialize || !fs.existsSync(__dirname + '/../admin/index_m.html') || !fs.existsSync(__dirname + '/../gulpfile.js')) {
console.log('WARNING: Admin3 support is missing! Please add it');
console.log();
}
if (ioPackage.common.materialize) {
expect(fs.existsSync(__dirname + '/../admin/index_m.html'), 'Admin3 support is enabled in io-package.json, but index_m.html is missing!').to.be.true;
}
}
var licenseFileExists = fs.existsSync(__dirname + '/../LICENSE');
var fileContentReadme = fs.readFileSync(__dirname + '/../README.md', 'utf8');
if (fileContentReadme.indexOf('## Changelog') === -1) {
console.log('Warning: The README.md should have a section ## Changelog');
console.log();
}
expect((licenseFileExists || fileContentReadme.indexOf('## License') !== -1), 'A LICENSE must exist as LICENSE file or as part of the README.md').to.be.true;
if (!licenseFileExists) {
console.log('Warning: The License should also exist as LICENSE file');
console.log();
}
if (fileContentReadme.indexOf('## License') === -1) {
console.log('Warning: The README.md should also have a section ## License to be shown in Admin3');
console.log();
}
done();
});
});

162
widgets/kodi.html Normal file
View File

@ -0,0 +1,162 @@
<!--
yunkong2.yunkong2.kodi Widget-Set
version: "0.0.6"
Copyright 10.2015-2016 instalator<vvvalt@mail.ru>
-->
<link rel="stylesheet" href="widgets/kodi/css/style.css" />
<script type="text/javascript" src="widgets/kodi/js/kodi.js"></script>
<script id="tplkodiProgressBar"
type="text/ejs"
class="vis-tpl"
data-vis-prev='<img style="width:110px; height:10px" src="widgets/kodi/img/w_icon/progress.png"></img>'
data-vis-attrs="oid_seek/id/;"
data-vis-set="kodi"
data-vis-type="val"
data-vis-name="Progress Bar">
<div class="vis-widget <%== this.data.attr('class') %>" style="width:200px; height:20px; overflow: initial" id="<%= this.data.attr('wid') %>" >
<% vis.binds.kodi.Progress(this.data.wid, this.view, this.data, this.style);%>
</div>
</script>
<script id="tplkodiPlaylist"
type="text/ejs"
class="vis-tpl"
data-vis-prev='<img style="width:110px; height:90px" src="widgets/kodi/img/w_icon/playlist.png"></img>'
data-vis-attrs="oid_playlist/id/;oid_position/id;oid_server/id/;"
data-vis-set="kodi"
data-vis-type="val"
data-vis-name="KODI Playlist">
<div class="vis-widget <%== this.data.attr('class') %>" style="width:500px; height:240px" id="<%= this.data.attr('wid') %>" >
<div id="kodiplaylist">
<ul id="playListContainer">
</ul>
</div>
<% vis.binds.kodi.Playlist(this.data.wid, this.view, this.data, this.style);%>
</div>
</script>
<script id="tplkodiThumbnail"
type="text/ejs"
class="vis-tpl"
data-vis-prev='<img style="width:110px; height:110px" src="widgets/kodi/img/defaultplaylist.png"></img>'
data-vis-attrs="oid_thumbnail/id/;oid_type/id/;oid_server/id/;"
data-vis-set="kodi"
data-vis-type="val"
data-vis-name="KODI Thumbnail">
<div class="vis-widget <%== this.data.attr('class') %>" style="width:110px; height:110px" id="<%= this.data.attr('wid') %>" >
<ul class="kodicover">
<li class="cover"></li>
</ul>
<% vis.binds.kodi.Thumbnail(this.data.wid, this.view, this.data, this.style);%>
</div>
</script>
<script id="tplkodiCodec"
type="text/ejs"
class="vis-tpl"
data-vis-prev='<img style="width:100px; height:50px" src="widgets/kodi/img/w_icon/codec.png"></img>'
data-vis-attrs="oid_codec/id/;"
data-vis-set="kodi"
data-vis-type="val"
data-vis-name="KODI codec info">
<div class="vis-widget <%== this.data.attr('class') %>" style="width:100px; height:50px" id="<%= this.data.attr('wid') %>" >
<ul class="kodiinfo">
<li class="codec"></li>
</ul>
<% vis.binds.kodi.CodecInfo(this.data.wid, this.view, this.data, this.style);%>
</div>
</script>
<script id="tplkodiAspect"
type="text/ejs"
class="vis-tpl"
data-vis-prev='<img style="width:67px; height:50px" src="widgets/kodi/img/w_icon/aspect.png"></img>'
data-vis-attrs="oid_aspect/id/;"
data-vis-set="kodi"
data-vis-type="val"
data-vis-name="KODI AspectRatio info">
<div class="vis-widget <%== this.data.attr('class') %>" style="width:70px; height:50px" id="<%= this.data.attr('wid') %>" >
<ul class="kodiinfo">
<li class="aspect"></li>
</ul>
<% vis.binds.kodi.AspectInfo(this.data.wid, this.view, this.data, this.style);%>
</div>
</script>
<script id="tplkodiResolut"
type="text/ejs"
class="vis-tpl"
data-vis-prev='<img style="width:67px; height:50px" src="widgets/kodi/img/w_icon/resolution.png"></img>'
data-vis-attrs="oid_resolut/id/;"
data-vis-set="kodi"
data-vis-type="val"
data-vis-name="KODI Video Resolution info">
<div class="vis-widget <%== this.data.attr('class') %>" style="width:70px; height:50px" id="<%= this.data.attr('wid') %>" >
<ul class="kodiinfo">
<li class="resolut"></li>
</ul>
<% vis.binds.kodi.ResolutInfo(this.data.wid, this.view, this.data, this.style);%>
</div>
</script>
<script id="tplkodiChannel"
type="text/ejs"
class="vis-tpl"
data-vis-prev='<img style="width:64px; height:50px" src="widgets/kodi/img/w_icon/channel.png"></img>'
data-vis-attrs="oid_channel/id/;"
data-vis-set="kodi"
data-vis-type="val"
data-vis-name="KODI Audio Channel info">
<div class="vis-widget <%== this.data.attr('class') %>" style="width:70px; height:50px" id="<%= this.data.attr('wid') %>" >
<ul class="kodiinfo">
<li class="channel"></li>
</ul>
<% vis.binds.kodi.ChannelInfo(this.data.wid, this.view, this.data, this.style);%>
</div>
</script>
<script id="tplkodiVideoCodec"
type="text/ejs"
class="vis-tpl"
data-vis-prev='<img style="width:100px; height:50px" src="widgets/kodi/img/w_icon/video_codec.png"></img>'
data-vis-attrs="oid_videocodec/id/;"
data-vis-set="kodi"
data-vis-type="val"
data-vis-name="KODI VideoCodec info">
<div class="vis-widget <%== this.data.attr('class') %>" style="width:100px; height:50px" id="<%= this.data.attr('wid') %>" >
<ul class="kodiinfo">
<li class="videocodec"></li>
</ul>
<% vis.binds.kodi.VideoCodec(this.data.wid, this.view, this.data, this.style);%>
</div>
</script>
<script id="tplkodiButton"
type="text/ejs"
class="vis-tpl"
data-vis-prev='<img style="width:110px; height:20px" src="widgets/kodi/img/w_icon/button.png"></img>'
data-vis-attrs="oid_play/id/;oid_speed/id;oid_prev/id;oid_next/id;oid_stop/id;oid_mute/id;oid_rpt/id;oid_shf/id;oid_seek/id;"
data-vis-set="kodi"
data-vis-type="val"
data-vis-name="KODI Button">
<div class="vis-widget <%== this.data.attr('class') %>" style="width:400px; height:50px" id="<%= this.data.attr('wid') %>" >
<ul id="kodicontrols">
<li class="prev"></li>
<li class="rewind"></li>
<li class="playpause"></li>
<li class="stop"></li>
<li class="forward"></li>
<li class="next"></li>
<li class="repeat"></li>
<li class="shuffle"></li>
</ul>
<% vis.binds.kodi.Button(this.data.wid, this.view, this.data, this.style);%>
</div>
</script>

108
widgets/kodi/css/style.css Normal file
View File

@ -0,0 +1,108 @@
.yunkong2.kodi-class {
font-style: italic;
}
.kodicover, .cover{
width: 100%;
height: 100%;
text-align:center;
background-position: center top;
background-repeat: no-repeat;
background-size: cover;
margin: 0;
padding: 0;
}
.cover.adef {
background-image: url("../img/defaultplaylist.png") !important;
background-position: center top;
background-repeat: no-repeat;
background-size: cover;
width:100%;
height:100%;
margin: 0;
padding: 0;
}
.cover.vdef {
background-image: url("../img/defaultvideo.png") !important;
background-position: center top;
background-repeat: no-repeat;
background-size: cover;
width:100%;
height:100%;
margin: 0;
padding: 0;
}
#progressbar {
width: 100%;
height: 100%;
}
#kodiplaylist {
height: 100%;
width: 100%;
overflow-x: hidden;
}
#kodiplaylist li:hover, #kodiplaylist li:nth-child(even):hover { background-color:#00bd9b; color:#fff; }
#kodiplaylist li.active{
background-color:#00bd9b;
color:#fff;
}
#kodiplaylist li img{
box-shadow: 0 0 5px 0px #FFF, 0 0 0 1px #000, 0 0 0 1px #484848;
border-radius: 5px;
}
#kodiplaylist ul {
margin: 0;
padding-left: 5px;
clear: both;
cursor: pointer;
}
#kodicontrols, .kodiinfo{
height: 100%;
width: 100%;
padding: 0px;
margin: 0px;
}
#kodicontrols li {
float:left;
display:inline-block;
width:50px;
height: 50px;
text-align:center;
}
.kodiinfo li{
width: 100%;
height: 100%;
text-align:center;
background-position: center top;
background-repeat: no-repeat;
background-size: cover;
}
.prev { background: url("../img/btn/btn_sprite.png") -48px -48px }
.prev:hover { background: url("../img/btn/btn_sprite.png") -48px -0px;}
.rewind { background: url("../img/btn/btn_sprite.png") -144px 48px;}
.rewind:hover { background: url("../img/btn/btn_sprite.png") -144px -0px;}
.playpause { background: url("../img/btn/btn_sprite.png") -192px -48px }
.playpause:hover { background: url("../img/btn/btn_sprite.png") -192px -0px;}
.playpause.play { background: url("../img/btn/btn_sprite.png") -240px -48px;}
.playpause.play:hover { background: url("../img/btn/btn_sprite.png") -240px -0px}
.stop { background: url("../img/btn/btn_sprite.png") -528px 48px;}
.stop:hover { background: url("../img/btn/btn_sprite.png") -528px -0px;}
.forward { background: url("../img/btn/btn_sprite.png") -96px -48px}
.forward:hover { background: url("../img/btn/btn_sprite.png") -96px -0px}
.next { background: url("../img/btn/btn_sprite.png") -0px -48px}
.next:hover { background: url("../img/btn/btn_sprite.png") -0px -0px}
.repeat { background: url("../img/btn/btn_sprite.png") -480px -48px;}
.repeat:hover { background: url("../img/btn/btn_sprite.png") -480px -0px;}
.repeat.one { background: url("../img/btn/btn_sprite.png") -432px -48px;}
.repeat.one:hover { background: url("../img/btn/btn_sprite.png") -432px -0px;}
.repeat.all { background: url("../img/btn/btn_sprite.png") -384px -48px;}
.repeat.all:hover { background: url("../img/btn/btn_sprite.png") -384px -0px;}
.shuffle { background: url("../img/btn/btn_sprite.png") -336px -48px;}
.shuffle:hover { background: url("../img/btn/btn_sprite.png") -336px -0px}
.shuffle.on { background: url("../img/btn/btn_sprite.png") -288px -48px;}
.shuffle.on:hover { background: url("../img/btn/btn_sprite.png") -288px -0px;}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1003 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 946 B

BIN
widgets/kodi/img/handle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
widgets/kodi/img/test.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Some files were not shown because too many files have changed in this diff Show More