Initial commit
17
.gitattributes
vendored
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
After Width: | Height: | Size: 16 KiB |
BIN
admin/remote.jpg
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
admin/web.jpg
Normal file
After Width: | Height: | Size: 94 KiB |
25
appveyor.yml
Normal 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
837
kodi.js
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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;}
|
BIN
widgets/kodi/img/aspectratio/1.33.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
widgets/kodi/img/aspectratio/1.37.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
widgets/kodi/img/aspectratio/1.66.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
widgets/kodi/img/aspectratio/1.78.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
widgets/kodi/img/aspectratio/1.85.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
widgets/kodi/img/aspectratio/2.20.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
widgets/kodi/img/aspectratio/2.35.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
widgets/kodi/img/aspectratio/2.40.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
widgets/kodi/img/aspectratio/2.55.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
widgets/kodi/img/aspectratio/2.76.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
widgets/kodi/img/audio/0.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
widgets/kodi/img/audio/1.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
widgets/kodi/img/audio/10.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
widgets/kodi/img/audio/2.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
widgets/kodi/img/audio/3.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
widgets/kodi/img/audio/4.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
widgets/kodi/img/audio/5.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
widgets/kodi/img/audio/6.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
widgets/kodi/img/audio/7.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
widgets/kodi/img/audio/8.png
Normal file
After Width: | Height: | Size: 1003 B |
BIN
widgets/kodi/img/audio/aac.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
widgets/kodi/img/audio/ac3.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
widgets/kodi/img/audio/aif.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
widgets/kodi/img/audio/aifc.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
widgets/kodi/img/audio/aiff.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
widgets/kodi/img/audio/alac.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
widgets/kodi/img/audio/ape.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
widgets/kodi/img/audio/avc.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
widgets/kodi/img/audio/cdda.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
widgets/kodi/img/audio/dca.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
widgets/kodi/img/audio/dts.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
widgets/kodi/img/audio/dtshd_hra.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
widgets/kodi/img/audio/dtshd_ma.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
widgets/kodi/img/audio/eac3.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
widgets/kodi/img/audio/flac.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
widgets/kodi/img/audio/mp1.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
widgets/kodi/img/audio/mp2.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
widgets/kodi/img/audio/mp3.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
widgets/kodi/img/audio/ogg.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
widgets/kodi/img/audio/pcm_bluray.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
widgets/kodi/img/audio/truehd.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
widgets/kodi/img/audio/vorbis.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
widgets/kodi/img/audio/wav.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
widgets/kodi/img/audio/wavpack.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
widgets/kodi/img/audio/wma.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
widgets/kodi/img/audio/wmapro.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
widgets/kodi/img/audio/wmav2.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
widgets/kodi/img/audio_icons.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
widgets/kodi/img/bg-track.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
widgets/kodi/img/btn/btn_sprite.png
Normal file
After Width: | Height: | Size: 45 KiB |
BIN
widgets/kodi/img/defaultplaylist.png
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
widgets/kodi/img/defaultvideo.png
Normal file
After Width: | Height: | Size: 45 KiB |
BIN
widgets/kodi/img/grey/next.png
Normal file
After Width: | Height: | Size: 427 B |
BIN
widgets/kodi/img/grey/pause.png
Normal file
After Width: | Height: | Size: 149 B |
BIN
widgets/kodi/img/grey/play.png
Normal file
After Width: | Height: | Size: 409 B |
BIN
widgets/kodi/img/grey/playlist.png
Normal file
After Width: | Height: | Size: 216 B |
BIN
widgets/kodi/img/grey/previous.png
Normal file
After Width: | Height: | Size: 434 B |
BIN
widgets/kodi/img/grey/replay-all.png
Normal file
After Width: | Height: | Size: 693 B |
BIN
widgets/kodi/img/grey/replay.png
Normal file
After Width: | Height: | Size: 692 B |
BIN
widgets/kodi/img/grey/volume-decrease.png
Normal file
After Width: | Height: | Size: 911 B |
BIN
widgets/kodi/img/grey/volume-increase.png
Normal file
After Width: | Height: | Size: 946 B |
BIN
widgets/kodi/img/handle.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
widgets/kodi/img/test.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
widgets/kodi/img/video/1080.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
widgets/kodi/img/video/480.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
widgets/kodi/img/video/4k.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
widgets/kodi/img/video/540.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
widgets/kodi/img/video/576.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
widgets/kodi/img/video/720.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
widgets/kodi/img/video/avc1.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
widgets/kodi/img/video/bluray.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
widgets/kodi/img/video/divx.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
widgets/kodi/img/video/dvd.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
widgets/kodi/img/video/flv.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
widgets/kodi/img/video/h264.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
widgets/kodi/img/video/hddvd.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
widgets/kodi/img/video/hdmv.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
widgets/kodi/img/video/mpeg1video.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
widgets/kodi/img/video/mpeg2video.png
Normal file
After Width: | Height: | Size: 3.0 KiB |