├── .gitignore
├── LICENSE
├── README.md
├── build-stats.groovy
├── build.xml
├── client-extjs
├── .gitignore
├── Readme.md
├── app.js
├── app.json
├── app
│ ├── Application.js
│ ├── Node.js
│ ├── Readme.md
│ ├── model
│ │ ├── Base.js
│ │ └── Readme.md
│ ├── store
│ │ ├── ArchiveOptions.js
│ │ ├── ConflictActions.js
│ │ ├── EpisodeOrder.js
│ │ ├── FileAgeFilters.js
│ │ ├── FileSizeFilters.js
│ │ ├── Languages.js
│ │ ├── LogLevels.js
│ │ ├── MediaLabels.js
│ │ ├── MovieDatabases.js
│ │ ├── Readme.md
│ │ ├── RenameActions.js
│ │ ├── ScriptSources.js
│ │ ├── SeriesDatabases.js
│ │ └── VideoLengthFilters.js
│ └── view
│ │ ├── main
│ │ ├── Main.js
│ │ ├── MainController.js
│ │ └── MainModel.js
│ │ ├── navigation
│ │ ├── Navigation.js
│ │ ├── NavigationController.js
│ │ └── NavigationModel.js
│ │ ├── task
│ │ ├── Task.js
│ │ ├── TaskController.js
│ │ └── TaskModel.js
│ │ ├── tasklogcat
│ │ ├── TaskLogCat.js
│ │ ├── TaskLogCatController.js
│ │ └── TaskLogCatModel.js
│ │ └── taskmanager
│ │ ├── TaskManager.js
│ │ ├── TaskManagerController.js
│ │ └── TaskManagerModel.js
├── build.xml
├── index.html
├── resources
│ ├── images
│ │ ├── clear.png
│ │ ├── configure.png
│ │ ├── environment.png
│ │ ├── error.png
│ │ ├── favicon.png
│ │ ├── generic.png
│ │ ├── help.png
│ │ ├── info.png
│ │ ├── license.png
│ │ ├── mediainfo.png
│ │ ├── ok.png
│ │ ├── preferences.png
│ │ ├── purchase.png
│ │ ├── revert.png
│ │ ├── run.png
│ │ ├── schedule.png
│ │ ├── select.png
│ │ ├── settings.png
│ │ ├── stop.png
│ │ └── user.png
│ └── main.css
└── workspace.json
├── ivy.xml
├── makefile
├── package.properties
├── package
├── generic
│ ├── start
│ └── task
├── qnap
│ ├── .gitignore
│ ├── icons
│ │ ├── filebot-node.png
│ │ ├── filebot-node_100.png
│ │ ├── filebot-node_80.png
│ │ └── filebot-node_gray.png
│ ├── package_routines
│ ├── qpkg.cfg
│ └── shared
│ │ ├── filebot-node-service.sh
│ │ ├── start
│ │ └── task
├── synology-dsm7
│ ├── conf
│ │ ├── privilege
│ │ └── resource
│ ├── scripts
│ │ ├── postinst
│ │ ├── postuninst
│ │ ├── postupgrade
│ │ ├── preinst
│ │ ├── preuninst
│ │ ├── preupgrade
│ │ └── start-stop-status
│ └── target
│ │ ├── bin
│ │ ├── filebot-node-start
│ │ └── filebot-node-task
│ │ └── client
│ │ ├── FileBot.NodeClient.js
│ │ ├── auth
│ │ ├── config
│ │ ├── environment.cgi
│ │ ├── execute.cgi
│ │ ├── folders.cgi
│ │ ├── help
│ │ └── enu
│ │ │ └── filebot_node_index.html
│ │ ├── helptoc.conf
│ │ ├── images
│ │ ├── filebot_node_16.png
│ │ ├── filebot_node_24.png
│ │ ├── filebot_node_256.png
│ │ ├── filebot_node_32.png
│ │ ├── filebot_node_48.png
│ │ ├── filebot_node_64.png
│ │ └── filebot_node_72.png
│ │ ├── kill.cgi
│ │ ├── output.cgi
│ │ ├── proxy_pass.cgi
│ │ ├── schedule.cgi
│ │ ├── state.cgi
│ │ ├── task.cgi
│ │ ├── tasks.cgi
│ │ ├── test.cgi
│ │ ├── texts
│ │ └── enu
│ │ │ └── strings
│ │ └── version.cgi
└── synology
│ ├── conf
│ ├── privilege
│ └── resource
│ ├── scripts
│ ├── postinst
│ ├── postuninst
│ ├── postupgrade
│ ├── preinst
│ ├── preuninst
│ ├── preupgrade
│ └── start-stop-status
│ └── target
│ ├── bin
│ ├── filebot-node-start
│ └── filebot-node-task
│ └── client
│ ├── auth
│ ├── config
│ ├── environment.cgi
│ ├── execute.cgi
│ ├── folders.cgi
│ ├── images
│ ├── filebot_node_16.png
│ ├── filebot_node_24.png
│ ├── filebot_node_256.png
│ ├── filebot_node_32.png
│ ├── filebot_node_48.png
│ ├── filebot_node_64.png
│ └── filebot_node_72.png
│ ├── kill.cgi
│ ├── output.cgi
│ ├── proxy_pass.cgi
│ ├── schedule.cgi
│ ├── state.cgi
│ ├── task.cgi
│ ├── tasks.cgi
│ ├── test.cgi
│ └── version.cgi
├── server-nodejs
├── README.md
├── app.js
├── data
│ └── .gitignore
├── package-lock.json
├── package.json
├── start.sh
└── task.sh
├── syno-dsm6.json
└── syno.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.gitignore.io
2 |
3 | ### OSX ###
4 | .DS_Store
5 | .AppleDouble
6 | .LSOverride
7 |
8 | # Icon must end with two \r
9 | Icon
10 |
11 |
12 | # Thumbnails
13 | ._*
14 |
15 | # Files that might appear in the root of a volume
16 | .DocumentRevisions-V100
17 | .fseventsd
18 | .Spotlight-V100
19 | .TemporaryItems
20 | .Trashes
21 | .VolumeIcon.icns
22 |
23 | # Directories potentially created on remote AFP share
24 | .AppleDB
25 | .AppleDesktop
26 | Network Trash Folder
27 | Temporary Items
28 | .apdisk
29 |
30 |
31 | ### Windows ###
32 | # Windows image file caches
33 | Thumbs.db
34 | ehthumbs.db
35 |
36 | # Folder config file
37 | Desktop.ini
38 |
39 | # Recycle Bin used on file shares
40 | $RECYCLE.BIN/
41 |
42 | # Windows Installer files
43 | *.cab
44 | *.msi
45 | *.msm
46 | *.msp
47 |
48 | # Windows shortcuts
49 | *.lnk
50 |
51 |
52 | ### Linux ###
53 | *~
54 |
55 | # KDE directory preferences
56 | .directory
57 |
58 | # Linux trash folder which might appear on any partition or disk
59 | .Trash-*
60 |
61 |
62 | ### ExtJs ###
63 | .architect
64 | bootstrap.json
65 | build/
66 | ext/
67 |
68 |
69 | ### Node ###
70 | # Logs
71 | logs
72 | *.log
73 |
74 | # Runtime data
75 | pids
76 | *.pid
77 | *.seed
78 |
79 | # Directory for instrumented libs generated by jscoverage/JSCover
80 | lib-cov
81 |
82 | # Coverage directory used by tools like istanbul
83 | coverage
84 |
85 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
86 | .grunt
87 |
88 | # node-waf configuration
89 | .lock-wscript
90 |
91 | # Compiled binary addons (http://nodejs.org/api/addons.html)
92 | build/Release
93 |
94 | # Dependency directory
95 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
96 | node_modules
97 |
98 | # More Excludes
99 | *.sublime-project
100 | *.sublime-workspace
101 | *.properties
102 | *.key
103 | .idea/
104 | .sencha/
105 | overrides/
106 | packages/
107 | log/
108 | dist/
109 | build/
110 | release/
111 | notes/
112 | lib/
113 |
114 | *.variables
115 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FileBot Node
2 | [](https://github.com/filebot/filebot-node/releases)
3 | [](https://www.filebot.net/linux/syno.html)
4 |
5 | ## Introduction
6 | FileBot Node is a server-side Node.js application that allows you to make `filebot` calls via a straight-forward ExtJS web application.
7 |
8 | 
9 |
10 | ## User Manual
11 | FileBot Node is available as Synology package via the [FileBot Package Source](https://www.filebot.net/forums/viewtopic.php?t=1802) and as generic Linux package for all other devices. Please refer to the [How To](https://www.filebot.net/forums/viewtopic.php?t=2733) manual if you need help getting started.
12 |
13 | ## Installation
14 | Add the following __Package Source__ to Synology DSM ► Package Center ► Settings ► Package Sources:
15 |
16 | https://get.filebot.net/syno/
17 |
18 | FileBot Node will work on any Linux device that can run `filebot` and `node` but some tinkering may be required. You will need to [download](https://github.com/filebot/filebot-node/releases) and unpack the `tar` package and start the node service yourself. See `start.sh` for details.
19 |
20 | A Docker image is available [here](https://hub.docker.com/r/rednoah/filebot/).
21 |
22 | ## Notes
23 | * Node.js is required for the server-side process
24 | * System authentication is implemented for Synology DSM and QNAP NAS
25 |
26 | ## Discussion
27 | Please visit the [FileBot Forums](https://www.filebot.net/forums/viewforum.php?f=13) if you need help with setting things up.
28 |
--------------------------------------------------------------------------------
/build-stats.groovy:
--------------------------------------------------------------------------------
1 | import groovy.json.*
2 |
3 | // get download count from GitHub API
4 | def json = new JsonSlurper().parse(new URL('https://api.github.com/repos/filebot/filebot-node/releases'))
5 |
6 | def stats = [
7 | download_count: json.assets.download_count.flatten().sum() ?: 0
8 | ]
9 |
10 | println stats
11 |
12 | // export stats to ant build
13 | stats.each{ p, v -> project.setProperty(p, v as String) }
14 |
--------------------------------------------------------------------------------
/build.xml:
--------------------------------------------------------------------------------
1 |
2 | ' + command + '
or Link or cURL.',
145 | buttons: Ext.MessageBox.OK,
146 | icon: Ext.MessageBox.INFO
147 | }).removeCls('x-unselectable') // HACK TO FIX UNSELECTABLE TEXT
148 | }, this)
149 |
150 | // display filebot version output after successful initialization
151 | const version = new Ext.util.DelayedTask(function() {
152 | // start fetching task data
153 | this.requestVersion()
154 | }, this)
155 | version.delay(250)
156 | },
157 |
158 |
159 |
160 | init_syno: function() {
161 | // DSM 7 proxy_pass.cgi
162 | this.getServerEndpoint = function(path) {
163 | return Ext.manifest.server.endpoint + path + '.cgi'
164 | }
165 |
166 | // init CSRF token for DSM 6.2.4
167 | Ext.Ajax.request({
168 | method: 'GET',
169 | url: '/webman/login.cgi',
170 | disableCaching: false,
171 | success: function (response) {
172 | const json = Ext.decode(response.responseText)
173 | const token = json['SynoToken']
174 |
175 | // add CSRF token to all subsequent requests
176 | if (token) {
177 | Ext.Ajax.setDefaultHeaders({
178 | 'X-SYNO-TOKEN': token
179 | })
180 | this.getPostEndpoint = function(path, parameters) {
181 | parameters['SynoToken'] = token
182 | return this.getServerEndpoint(path) + '?' + Ext.Object.toQueryString(parameters)
183 | }
184 | }
185 |
186 | // run normal init code after login.cgi has been called
187 | this.init_generic()
188 | },
189 | failure: function (response) {
190 | Ext.MessageBox.show({
191 | title: 'Login Error',
192 | msg: response.responseText ? response.responseText : Ext.encode(response),
193 | buttons: Ext.MessageBox.OK,
194 | icon: Ext.MessageBox.ERROR
195 | })
196 | },
197 | scope: this
198 | })
199 |
200 | // Task Scheduler Web API doesn't accept requests from localhost so we have to do it from the browser
201 | FileBot.getApplication().on('schedule', function(request) {
202 | const name = 'FileBot Task ' + request.id
203 | const command = request.command
204 |
205 | // Syno Web API rejects requests from localhost, so we have to send the request from the client
206 | Ext.Ajax.request({
207 | method: 'POST',
208 | url: '/webapi/_______________________________________________________entry.cgi',
209 | params: {
210 | name: JSON.stringify(name),
211 | real_owner: JSON.stringify('admin'),
212 | owner: JSON.stringify('admin'),
213 | enable: true,
214 | schedule: JSON.stringify({"date_type":0,"week_day":"0,1,2,3,4,5,6","hour":4,"minute":0,"repeat_hour":0,"repeat_min":0,"last_work_hour":0,"repeat_min_store_config":[1,5,10,15,20,30],"repeat_hour_store_config":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23]}),
215 | extra: JSON.stringify({"notify_enable":false,"script":command,"notify_mail":"","notify_if_error":false}),
216 | type: JSON.stringify('script'),
217 | api: 'SYNO.Core.TaskScheduler',
218 | method: 'create',
219 | version: 3
220 | },
221 | success: function (response) {
222 | // e.g. {"error":{"code":104},"success":false}
223 | const json = Ext.JSON.decode(response.responseText)
224 | if (json.success) {
225 | Ext.create('Ext.window.MessageBox', {
226 | // set closeAction to 'destroy' if this instance is not
227 | // intended to be reused by the application
228 | closeAction: 'destroy'
229 | }).show({
230 | title: 'Synology Task Scheduler',
231 | msg: '' + name + ' has been added to the Synology Task Scheduler. Please use Control Panel ➔ Task Scheduler to modify or delete this task.',
232 | buttons: Ext.MessageBox.OK,
233 | icon: Ext.MessageBox.INFO
234 | })
235 | }
236 | },
237 | scope: this
238 | })
239 | }, this)
240 | }
241 |
242 | });
243 |
--------------------------------------------------------------------------------
/client-extjs/app/Readme.md:
--------------------------------------------------------------------------------
1 | # ./controller
2 |
3 | This folder contains the application's global controllers. ViewControllers are located
4 | alongside their respective view class in `"./view"`. These controllers are used for routing
5 | and other activities that span all views.
6 |
7 | # ./model
8 |
9 | This folder contains the application's (data) Model classes.
10 |
11 | # ./view
12 |
13 | This folder contains the views as well as ViewModels and ViewControllers depending on the
14 | application's architecture. Pure MVC applications may not have ViewModels, for example. For
15 | MVCVM applications or MVC applications that use ViewControllers, the following directory
16 | structure is recommended:
17 |
18 | ./view/
19 | foo/ # Some meaningful grouping of one or more views
20 | Foo.js # The view class
21 | FooController.js # The controller for Foo (a ViewController)
22 | FooModel.js # The ViewModel for Foo
23 |
24 | This structure helps keep these closely related classes together and easily identifiable in
25 | most tabbed IDE's or text editors.
26 |
27 | # ./store
28 |
29 | This folder contains any number of store instances or types that can then be reused in the
30 | application.
31 |
--------------------------------------------------------------------------------
/client-extjs/app/model/Base.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This model is the base for all other models in this application.
3 | */
4 | Ext.define('FileBot.model.Base', {
5 | extend: 'Ext.data.Model',
6 |
7 | schema: {
8 | namespace: 'FileBot.model'
9 | }
10 | });
11 |
--------------------------------------------------------------------------------
/client-extjs/app/model/Readme.md:
--------------------------------------------------------------------------------
1 | This folder contains the Models for this application.
2 |
--------------------------------------------------------------------------------
/client-extjs/app/store/ArchiveOptions.js:
--------------------------------------------------------------------------------
1 | Ext.define('FileBot.store.ArchiveOptions', {
2 | extend: 'Ext.data.ArrayStore',
3 | alias: 'store.archive-options',
4 | storeId: 'archive-options',
5 |
6 | fields: [
7 | 'id', 'value', 'label'
8 | ],
9 |
10 | data: [
11 | [0, 'skip', 'ignore archives'],
12 | [1, 'extract-keep', 'extract and keep archives'],
13 | [2, 'extract-delete', 'extract and delete archives']
14 | ]
15 | });
16 |
--------------------------------------------------------------------------------
/client-extjs/app/store/ConflictActions.js:
--------------------------------------------------------------------------------
1 | Ext.define('FileBot.store.ConflictActions', {
2 | extend: 'Ext.data.ArrayStore',
3 | alias: 'store.conflict-actions',
4 | storeId: 'conflict-actions',
5 |
6 | fields: [
7 | 'id', 'value', 'label'
8 | ],
9 |
10 | data: [
11 | [0, 'skip', 'skip existing files'],
12 | [1, 'replace', 'overwrite existing files'],
13 | [2, 'auto', 'overwrite if new file is better'],
14 | [3, 'index', 'keep both and index new file'],
15 | [4, 'fail', 'fail if files already exist']
16 | ]
17 | });
18 |
--------------------------------------------------------------------------------
/client-extjs/app/store/EpisodeOrder.js:
--------------------------------------------------------------------------------
1 | Ext.define('FileBot.store.EpisodeOrders', {
2 | extend: 'Ext.data.ArrayStore',
3 | alias: 'store.episode-orders',
4 | storeId: 'episode-orders',
5 |
6 | fields: [
7 | 'id', 'value', 'label'
8 | ],
9 |
10 | data: [
11 | [0, 'Airdate', 'Airdate'],
12 | [1, 'DVD', 'DVD'],
13 | [2, 'Absolute', 'Absolute'],
14 | [3, 'Digital', 'Digital'],
15 | [4, 'Story', 'Story Arc'],
16 | [5, 'Production', 'Production'],
17 | [6, 'Date', 'Date and Title']
18 | ]
19 | });
20 |
--------------------------------------------------------------------------------
/client-extjs/app/store/FileAgeFilters.js:
--------------------------------------------------------------------------------
1 | Ext.define('FileBot.store.FileAgeFilters', {
2 | extend: 'Ext.data.ArrayStore',
3 | alias: 'store.fileage-filters',
4 | storeId: 'fileage-filters',
5 |
6 | fields: [
7 | 'id', 'value', 'label'
8 | ],
9 |
10 | data: [
11 | [1, '', 'default'],
12 | [2, '0.125', '3 hours'],
13 | [3, '0.5', '12 hours'],
14 | [4, '1', '24 hours'],
15 | [5, '3', '3 days'],
16 | [6, '7', '7 days']
17 | ]
18 | });
19 |
--------------------------------------------------------------------------------
/client-extjs/app/store/FileSizeFilters.js:
--------------------------------------------------------------------------------
1 | Ext.define('FileBot.store.FileSizeFilters', {
2 | extend: 'Ext.data.ArrayStore',
3 | alias: 'store.filesize-filters',
4 | storeId: 'filesize-filters',
5 |
6 | fields: [
7 | 'id', 'value', 'label'
8 | ],
9 |
10 | data: [
11 | [1, '', 'default'],
12 | [2, ''+0, '0 bytes'],
13 | [3, ''+100*1000*1000, '100 MB'],
14 | [4, ''+500*1000*1000, '500 MB'],
15 | [5, ''+2*1000*1000*1000, '2 GB'],
16 | [6, ''+5*1000*1000*1000, '5 GB']
17 | ]
18 | });
19 |
--------------------------------------------------------------------------------
/client-extjs/app/store/Languages.js:
--------------------------------------------------------------------------------
1 | Ext.define('FileBot.store.Languages', {
2 | extend: 'Ext.data.ArrayStore',
3 | alias: 'store.languages',
4 | storeId: 'languages',
5 |
6 | fields: [
7 | 'id', 'iso_639_1', 'iso_639_3', 'iso_639_2B', 'label'
8 | ],
9 |
10 | data: [
11 | [ 1, 'sq', 'sqi', 'alb', 'Albanian'],
12 | [ 2, 'ar', 'ara', 'ara', 'Arabic'],
13 | [ 3, 'hy', 'hye', 'arm', 'Armenian'],
14 | [ 4, 'pb', 'pob', 'pob', 'Brazilian'],
15 | [ 5, 'bg', 'bul', 'bul', 'Bulgarian'],
16 | [ 6, 'ca', 'cat', 'cat', 'Catalan'],
17 | [ 7, 'zh', 'zho', 'chi', 'Chinese'],
18 | [ 8, 'hr', 'hrv', 'hrv', 'Croatian'],
19 | [ 9, 'cs', 'ces', 'cze', 'Czech'],
20 | [10, 'da', 'dan', 'dan', 'Danish'],
21 | [11, 'nl', 'nld', 'dut', 'Dutch'],
22 | [12, 'en', 'eng', 'eng', 'English'],
23 | [13, 'et', 'est', 'est', 'Estonian'],
24 | [14, 'fi', 'fin', 'fin', 'Finnish'],
25 | [15, 'fr', 'fra', 'fre', 'French'],
26 | [16, 'de', 'deu', 'ger', 'German'],
27 | [17, 'el', 'ell', 'gre', 'Greek'],
28 | [18, 'he', 'heb', 'heb', 'Hebrew'],
29 | [19, 'hi', 'hin', 'hin', 'Hindi'],
30 | [20, 'hu', 'hun', 'hun', 'Hungarian'],
31 | [21, 'id', 'ind', 'ind', 'Indonesian'],
32 | [22, 'it', 'ita', 'ita', 'Italian'],
33 | [23, 'ja', 'jpn', 'jpn', 'Japanese'],
34 | [24, 'ko', 'kor', 'kor', 'Korean'],
35 | [25, 'lv', 'lav', 'lav', 'Latvian'],
36 | [26, 'lt', 'lit', 'lit', 'Lithuanian'],
37 | [27, 'mk', 'mkd', 'mac', 'Macedonian'],
38 | [28, 'ms', 'msa', 'may', 'Malay'],
39 | [29, 'no', 'nor', 'nor', 'Norwegian'],
40 | [30, 'fa', 'fas', 'per', 'Persian'],
41 | [31, 'pl', 'pol', 'pol', 'Polish'],
42 | [32, 'pt', 'por', 'por', 'Portuguese'],
43 | [33, 'ro', 'ron', 'rum', 'Romanian'],
44 | [34, 'ru', 'rus', 'rus', 'Russian'],
45 | [35, 'sr', 'srp', 'srp', 'Serbian'],
46 | [36, 'sk', 'slk', 'slo', 'Slovak'],
47 | [37, 'sl', 'slv', 'slv', 'Slovenian'],
48 | [38, 'es', 'spa', 'spa', 'Spanish'],
49 | [39, 'sv', 'swe', 'swe', 'Swedish'],
50 | [40, 'th', 'tha', 'tha', 'Thai'],
51 | [41, 'tr', 'tur', 'tur', 'Turkish'],
52 | [42, 'vi', 'vie', 'vie', 'Vietnamese']
53 | ]
54 | });
55 |
--------------------------------------------------------------------------------
/client-extjs/app/store/LogLevels.js:
--------------------------------------------------------------------------------
1 | Ext.define('FileBot.store.LogLevels', {
2 | extend: 'Ext.data.ArrayStore',
3 | alias: 'store.log-levels',
4 | storeId: 'log-levels',
5 |
6 | fields: [
7 | 'id', 'value', 'label'
8 | ],
9 |
10 | data: [
11 | [0, 'info', 'only results and errors'],
12 | [1, 'fine', 'all important messages'],
13 | [2, 'all', 'everything']
14 | ]
15 | });
16 |
--------------------------------------------------------------------------------
/client-extjs/app/store/MediaLabels.js:
--------------------------------------------------------------------------------
1 | Ext.define('FileBot.store.MediaLabels', {
2 | extend: 'Ext.data.ArrayStore',
3 | alias: 'store.media-labels',
4 | storeId: 'media-labels',
5 |
6 | fields: [
7 | 'id', 'value', 'label'
8 | ],
9 |
10 | data: [
11 | [0, '', 'Automatic'],
12 | [1, 'Movie', 'Movies'],
13 | [2, 'TV', 'TV Series'],
14 | [3, 'Anime', 'Anime'],
15 | [4, 'Music', 'Music'],
16 | [5, 'other', 'Files']
17 | ]
18 | });
19 |
--------------------------------------------------------------------------------
/client-extjs/app/store/MovieDatabases.js:
--------------------------------------------------------------------------------
1 | Ext.define('FileBot.store.MovieDatabases', {
2 | extend: 'Ext.data.ArrayStore',
3 | alias: 'store.movie-databases',
4 | storeId: 'movie-databases',
5 |
6 | fields: [
7 | 'id', 'value', 'label'
8 | ],
9 |
10 | data: [
11 | [1, '', 'default'],
12 | [2, 'TheMovieDB', 'TheMovieDB'],
13 | [3, 'OMDb', 'OMDb']
14 | ]
15 | });
16 |
--------------------------------------------------------------------------------
/client-extjs/app/store/Readme.md:
--------------------------------------------------------------------------------
1 | This folder contains store instances (identified by storeId) and store types
2 | (with "store.foo" aliases).
3 |
--------------------------------------------------------------------------------
/client-extjs/app/store/RenameActions.js:
--------------------------------------------------------------------------------
1 | Ext.define('FileBot.store.RenameActions', {
2 | extend: 'Ext.data.ArrayStore',
3 | alias: 'store.rename-actions',
4 | storeId: 'rename-actions',
5 |
6 | fields: [
7 | 'id', 'value', 'label'
8 | ],
9 |
10 | data: [
11 | [1, 'move', 'move and rename'],
12 | [2, 'copy', 'copy'],
13 | [3, 'hardlink', 'hardlink'],
14 | [4, 'symlink', 'symlink'],
15 | [5, 'clone', 'reflink'],
16 | [6, 'keeplink', 'move and symlink back'],
17 | [7, 'duplicate', 'hardlink or copy']
18 | ]
19 | });
20 |
--------------------------------------------------------------------------------
/client-extjs/app/store/ScriptSources.js:
--------------------------------------------------------------------------------
1 | Ext.define('FileBot.store.ScriptSources', {
2 | extend: 'Ext.data.ArrayStore',
3 | alias: 'store.script-sources',
4 | storeId: 'script-sources',
5 |
6 | fields: [
7 | 'id', 'value', 'label'
8 | ],
9 |
10 | data: [
11 | [0, 'fn', 'stable'],
12 | [1, 'dev', 'latest']
13 | ]
14 | });
15 |
--------------------------------------------------------------------------------
/client-extjs/app/store/SeriesDatabases.js:
--------------------------------------------------------------------------------
1 | Ext.define('FileBot.store.SeriesDatabases', {
2 | extend: 'Ext.data.ArrayStore',
3 | alias: 'store.series-databases',
4 | storeId: 'series-databases',
5 |
6 | fields: [
7 | 'id', 'value', 'label'
8 | ],
9 |
10 | data: [
11 | [1, '', 'default'],
12 | [2, 'TheMovieDB::TV', 'TheMovieDB'],
13 | [3, 'AniDB', 'AniDB'],
14 | [4, 'TheTVDB', 'TheTVDB'],
15 | [5, 'TVmaze', 'TVmaze']
16 | ]
17 | });
18 |
--------------------------------------------------------------------------------
/client-extjs/app/store/VideoLengthFilters.js:
--------------------------------------------------------------------------------
1 | Ext.define('FileBot.store.VideoLengthFilters', {
2 | extend: 'Ext.data.ArrayStore',
3 | alias: 'store.videolength-filters',
4 | storeId: 'videolength-filters',
5 |
6 | fields: [
7 | 'id', 'value', 'label'
8 | ],
9 |
10 | data: [
11 | [1, '', 'default'],
12 | [2, ''+0, '0 seconds'],
13 | [3, ''+5*60*1000, '5 minutes'],
14 | [4, ''+15*60*1000, '15 minutes'],
15 | [5, ''+30*60*1000, '30 minutes'],
16 | [6, ''+60*60*1000, '60 minutes'],
17 | [7, ''+90*60*1000, '90 minutes']
18 | ]
19 | });
20 |
--------------------------------------------------------------------------------
/client-extjs/app/view/main/Main.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is the main view for the application. It is specified in app.js as the
3 | * "autoCreateViewport" property. That setting automatically applies the "viewport"
4 | * plugin to promote that instance of this class to the body element.
5 | *
6 | * TODO - Replace this content of this view to suite the needs of your application.
7 | */
8 | Ext.define('FileBot.view.main.Main', {
9 | extend: 'Ext.container.Container',
10 | requires: [
11 | 'FileBot.view.main.MainController',
12 | 'FileBot.view.main.MainModel',
13 | 'FileBot.view.navigation.Navigation',
14 | 'FileBot.view.task.Task'
15 | ],
16 |
17 | xtype: 'app-main',
18 |
19 | controller: 'main',
20 | viewModel: {
21 | type: 'main'
22 | },
23 |
24 | layout: {
25 | type: 'border'
26 | },
27 |
28 | items: [{
29 | region: 'center',
30 | xtype: 'section-task'
31 | }]
32 | });
33 |
--------------------------------------------------------------------------------
/client-extjs/app/view/main/MainController.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is the main view for the application. It is specified in app.js as the
3 | * "autoCreateViewport" property. That setting automatically applies the "viewport"
4 | * plugin to promote that instance of this class to the body element.
5 | *
6 | * TODO - Replace this content of this view to suite the needs of your application.
7 | */
8 | Ext.define('FileBot.view.main.MainController', {
9 | extend: 'Ext.app.ViewController',
10 |
11 | requires: [
12 |
13 | ],
14 |
15 | alias: 'controller.main',
16 |
17 | /**
18 | * Called when the view is created
19 | */
20 | init: function() {
21 | if (Ext.util.LocalStorage.supported) {
22 | Ext.state.Manager.setProvider(new Ext.state.LocalStorageProvider())
23 | } else {
24 | Ext.state.Manager.setProvider(new Ext.state.CookieProvider())
25 | }
26 | }
27 |
28 | });
29 |
--------------------------------------------------------------------------------
/client-extjs/app/view/main/MainModel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is the view model for the Main view of the application.
3 | */
4 | Ext.define('FileBot.view.main.MainModel', {
5 | extend: 'Ext.app.ViewModel',
6 |
7 | alias: 'viewmodel.main',
8 |
9 | data: {
10 | name: 'FileBot'
11 | }
12 |
13 | //TODO - add data, formulas and/or methods to support your view
14 | });
15 |
--------------------------------------------------------------------------------
/client-extjs/app/view/navigation/Navigation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by reinhard on 4/25/15.
3 | */
4 | Ext.define('FileBot.view.navigation.Navigation', {
5 | extend: 'Ext.tab.Panel',
6 | xtype: 'navigation-tabs',
7 |
8 | // ui: 'navigation', // NOT WORKING
9 |
10 | tabBar: {
11 | layout: {
12 | pack: 'center'
13 | }
14 | },
15 |
16 | defaults: {
17 | iconAlign: 'top',
18 | bodyPadding: 15
19 | }
20 |
21 | });
22 |
--------------------------------------------------------------------------------
/client-extjs/app/view/navigation/NavigationController.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by reinhard on 4/25/15.
3 | */
4 | Ext.define('FileBot.view.navigation.NavigationController', {
5 | extend: 'Ext.app.ViewController',
6 | alias: 'controller.navigation',
7 |
8 | /**
9 | * Called when the view is created
10 | */
11 | init: function() {
12 |
13 | }
14 | });
15 |
--------------------------------------------------------------------------------
/client-extjs/app/view/navigation/NavigationModel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by reinhard on 4/25/15.
3 | */
4 | Ext.define('FileBot.view.navigation.NavigationModel', {
5 | extend: 'Ext.app.ViewModel',
6 | alias: 'viewmodel.navigation',
7 |
8 | stores: {
9 | /*
10 | A declaration of Ext.data.Store configurations that are first processed as binds to produce an effective
11 | store configuration. For example:
12 |
13 | users: {
14 | model: 'Navigation',
15 | autoLoad: true
16 | }
17 | */
18 | },
19 |
20 | data: {
21 | /* This object holds the arbitrary data that populates the ViewModel and is then available for binding. */
22 | }
23 | });
24 |
--------------------------------------------------------------------------------
/client-extjs/app/view/task/Task.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is the main view for the application. It is specified in app.js as the
3 | * "autoCreateViewport" property. That setting automatically applies the "viewport"
4 | * plugin to promote that instance of this class to the body element.
5 | *
6 | * TODO - Replace this content of this view to suite the needs of your application.
7 | */
8 | Ext.define('FileBot.view.task.Task', {
9 | extend: 'Ext.container.Container',
10 | requires: [
11 | 'FileBot.Node',
12 | 'FileBot.view.task.TaskController',
13 | 'FileBot.view.task.TaskModel',
14 | 'FileBot.view.taskmanager.TaskManager',
15 | 'FileBot.view.tasklogcat.TaskLogCat',
16 | 'FileBot.store.RenameActions',
17 | 'FileBot.store.MediaLabels',
18 | 'FileBot.store.EpisodeOrders',
19 | 'FileBot.store.LogLevels',
20 | 'FileBot.store.ArchiveOptions',
21 | 'FileBot.store.VideoLengthFilters',
22 | 'FileBot.store.FileSizeFilters',
23 | 'FileBot.store.FileAgeFilters',
24 | 'FileBot.store.Languages',
25 | 'FileBot.store.ConflictActions',
26 | 'FileBot.store.ScriptSources',
27 | 'FileBot.store.MovieDatabases',
28 | 'FileBot.store.SeriesDatabases'
29 | ],
30 |
31 | xtype: 'section-task',
32 |
33 | controller: 'task',
34 | viewModel: {
35 | type: 'task'
36 | },
37 |
38 | listeners: {
39 | afterrender: 'restoreState'
40 | },
41 |
42 | defaults: {
43 | collapsible: true,
44 | split: true,
45 | scrollable: true,
46 | floatable: false,
47 | bodyPadding: 10
48 | },
49 |
50 | layout: 'border',
51 |
52 | items: [{
53 | region: 'center',
54 | xtype: 'form',
55 | title: 'Organize Files',
56 | headerPosition: 'left',
57 | bodyPadding: 20,
58 | collapsible: false,
59 |
60 | defaults: {
61 | xtype: 'fieldset',
62 | collapsible: true,
63 | collapsed: true
64 | },
65 | items: [{
66 | title: 'Organize Files',
67 | collapsible: false,
68 | collapsed: false,
69 |
70 | defaults: {
71 | allowBlank: false,
72 | forceSelection: true,
73 | queryMode: 'local'
74 | },
75 | items: [{
76 | xtype: 'hidden',
77 | name: 'fn',
78 | value: 'amc',
79 | hidden: true
80 | }, {
81 | xtype: 'combobox',
82 | name: 'input',
83 | fieldLabel: 'Input Folder',
84 | emptyText: '/path/to/input',
85 | value: Ext.manifest.server.form.input,
86 | bind: {
87 | store: '{folders}'
88 | },
89 | displayField: 'path',
90 | valueField: 'path',
91 | minChars: 0, // forcing the query to run every time by setting minChars to 0
92 | queryCaching: true,
93 | queryParam: 'q',
94 | queryMode: 'remote',
95 | forceSelection: false,
96 | editable: true,
97 | anchor: '100%'
98 | }, {
99 | xtype: 'combobox',
100 | name: 'label',
101 | fieldLabel: 'Input Type',
102 | displayField: 'label',
103 | valueField: 'value',
104 | value: '',
105 | store: {
106 | type: 'media-labels'
107 | },
108 | editable: false,
109 | minWidth: 280
110 | }, {
111 | xtype: 'checkboxfield',
112 | name: 'strict',
113 | fieldLabel: 'Strict Mode',
114 | boxLabel: 'use strict mode',
115 | inputValue: 'no',
116 | checked: false
117 | }, {
118 | xtype: 'combobox',
119 | name: 'action',
120 | fieldLabel: 'Action',
121 | displayField: 'label',
122 | valueField: 'value',
123 | value: 'duplicate',
124 | store: {
125 | type: 'rename-actions'
126 | },
127 | editable: true,
128 | minWidth: 280
129 | }, {
130 | xtype: 'combobox',
131 | name: 'output',
132 | fieldLabel: 'Output Folder',
133 | emptyText: '/path/to/output',
134 | value: Ext.manifest.server.form.output,
135 | bind: {
136 | store: '{folders}'
137 | },
138 | displayField: 'path',
139 | valueField: 'path',
140 | minChars: 0, // forcing the query to run every time by setting minChars to 0
141 | queryCaching: true,
142 | queryParam: 'q',
143 | queryMode: 'remote',
144 | forceSelection: false,
145 | editable: true,
146 | anchor: '100%'
147 | }, {
148 | xtype: 'checkboxfield',
149 | name: 'artwork',
150 | fieldLabel: 'Artwork',
151 | boxLabel: 'fetch artwork and generate .nfo files',
152 | checked: false
153 | }, {
154 | xtype: 'checkboxfield',
155 | name: 'clean',
156 | fieldLabel: 'Clean',
157 | boxLabel: 'delete left behind clutter files',
158 | checked: false
159 | }, {
160 | xtype: 'combobox',
161 | name: 'order',
162 | fieldLabel: 'Episode Order',
163 | displayField: 'label',
164 | valueField: 'value',
165 | value: 'Airdate',
166 | store: {
167 | type: 'episode-orders'
168 | },
169 | editable: false,
170 | minWidth: 280
171 | }, {
172 | xtype: 'combobox',
173 | name: 'subtitles',
174 | fieldLabel: 'Subtitles',
175 | displayField: 'label',
176 | valueField: 'iso_639_3',
177 | store: {
178 | type: 'languages'
179 | },
180 | emptyText: 'subtitle language',
181 | forceSelection: false,
182 | editable: true,
183 | allowBlank: true,
184 | minWidth: 320
185 | }, {
186 | xtype: 'combobox',
187 | name: 'lang',
188 | fieldLabel: 'Language',
189 | displayField: 'label',
190 | valueField: 'iso_639_1',
191 | value: 'en',
192 | store: {
193 | type: 'languages'
194 | },
195 | emptyText: 'language',
196 | editable: false,
197 | minWidth: 320
198 | }]
199 | }, {
200 | title: 'Media Server',
201 | collapsible: false,
202 | collapsed: false,
203 |
204 | id: 'media-server-options',
205 | hidden: true,
206 |
207 | items: [{
208 | xtype: 'checkboxfield',
209 | name: 'thumbnail',
210 | fieldLabel: 'Thumbnails',
211 | boxLabel: 'generate thumbnails',
212 | inputValue: 'on',
213 | uncheckedValue: 'no',
214 | checked: false
215 | }, {
216 | xtype: 'checkboxfield',
217 | name: 'refresh',
218 | fieldLabel: 'Re-index',
219 | boxLabel: 'refresh file services',
220 | inputValue: 'on',
221 | uncheckedValue: 'no',
222 | checked: false
223 | }]
224 | }, {
225 | title: 'File Options',
226 | defaults: {
227 | allowBlank: true,
228 | forceSelection: true,
229 | queryMode: 'local'
230 | },
231 | items: [{
232 | xtype: 'combobox',
233 | name: 'conflict',
234 | fieldLabel: 'Conflict',
235 | labelStyle: 'white-space: nowrap; width: 120px;',
236 | displayField: 'label',
237 | valueField: 'value',
238 | value: 'auto',
239 | store: {
240 | type: 'conflict-actions'
241 | },
242 | editable: true,
243 | minWidth: 320
244 | }, {
245 | xtype: 'combobox',
246 | name: 'archives',
247 | fieldLabel: 'Archives',
248 | labelStyle: 'white-space: nowrap; width: 120px;',
249 | displayField: 'label',
250 | valueField: 'value',
251 | value: 'skip',
252 | store: {
253 | type: 'archive-options'
254 | },
255 | editable: false,
256 | minWidth: 320
257 | }, {
258 | xtype: 'checkboxfield',
259 | name: 'music',
260 | fieldLabel: 'Music',
261 | labelStyle: 'white-space: nowrap; width: 120px;',
262 | boxLabel: 'skip music files',
263 | inputValue: 'no',
264 | checked: false
265 | }, {
266 | xtype: 'checkboxfield',
267 | name: 'unsorted',
268 | fieldLabel: 'Unsorted Files',
269 | labelStyle: 'white-space: nowrap; width: 120px;',
270 | boxLabel: 'skip unsorted files',
271 | inputValue: 'no',
272 | checked: false
273 | }, {
274 | xtype: 'textfield',
275 | name: 'ignore',
276 | fieldLabel: 'Ignore Rules',
277 | labelStyle: 'white-space: nowrap; width: 120px;',
278 | emptyText: 'games|books',
279 | anchor: '100%'
280 | }, {
281 | xtype: 'combobox',
282 | name: 'minLengthMS',
283 | fieldLabel: 'Video Duration',
284 | labelStyle: 'white-space: nowrap; width: 120px;',
285 | displayField: 'label',
286 | valueField: 'value',
287 | value: '',
288 | store: {
289 | type: 'videolength-filters'
290 | },
291 | editable: true
292 | }, {
293 | xtype: 'combobox',
294 | name: 'minFileSize',
295 | fieldLabel: 'File Size',
296 | labelStyle: 'white-space: nowrap; width: 120px;',
297 | displayField: 'label',
298 | valueField: 'value',
299 | value: '',
300 | store: {
301 | type: 'filesize-filters'
302 | },
303 | editable: true
304 | }, {
305 | xtype: 'combobox',
306 | name: 'minFileAge',
307 | fieldLabel: 'File Age',
308 | labelStyle: 'white-space: nowrap; width: 120px;',
309 | displayField: 'label',
310 | valueField: 'value',
311 | value: '',
312 | store: {
313 | type: 'fileage-filters'
314 | },
315 | editable: true
316 | }, {
317 | xtype: 'checkboxfield',
318 | name: 'excludeLink',
319 | fieldLabel: 'Exclude Link',
320 | labelStyle: 'white-space: nowrap; width: 120px;',
321 | boxLabel: 'skip superfluous links',
322 | checked: false
323 | }, {
324 | xtype: 'textfield',
325 | name: 'excludeList',
326 | fieldLabel: 'Exclude List',
327 | labelStyle: 'white-space: nowrap; width: 120px;',
328 | emptyText: 'exclude file that keeps track of processed files',
329 | value: '.excludes',
330 | anchor: '100%'
331 | }]
332 | }, {
333 | title: 'Match Options',
334 | defaults: {
335 | allowBlank: true,
336 | xtype: 'textfield',
337 | anchor: '100%'
338 | },
339 | items: [{
340 | xtype: 'textfield',
341 | name: 'query',
342 | fieldLabel: 'Query Expression',
343 | labelStyle: 'white-space: nowrap; width: 120px;',
344 | emptyText: '70327',
345 | allowBlank: true,
346 | anchor: '100%'
347 | }, {
348 | xtype: 'textfield',
349 | name: 'filter',
350 | fieldLabel: 'Match Filter',
351 | labelStyle: 'white-space: nowrap; width: 120px;',
352 | emptyText: 'age < 7',
353 | allowBlank: true,
354 | anchor: '100%'
355 | }, {
356 | xtype: 'textfield',
357 | name: 'mapper',
358 | fieldLabel: 'Match Mapper',
359 | labelStyle: 'white-space: nowrap; width: 120px;',
360 | emptyText: 'order.absolute.episode',
361 | allowBlank: true,
362 | anchor: '100%'
363 | }]
364 | }, {
365 | title: 'Format Options',
366 | defaults: {
367 | allowBlank: true,
368 | xtype: 'textfield',
369 | anchor: '100%'
370 | },
371 | items: [{
372 | fieldLabel: 'Movie Format',
373 | labelStyle: 'white-space: nowrap; width: 120px;',
374 | name: 'movieFormat',
375 | emptyText: '{ plex.id }'
376 | }, {
377 | fieldLabel: 'Series Format',
378 | labelStyle: 'white-space: nowrap; width: 120px;',
379 | name: 'seriesFormat',
380 | emptyText: '{ plex.id }'
381 | }, {
382 | fieldLabel: 'Anime Format',
383 | labelStyle: 'white-space: nowrap; width: 120px;',
384 | name: 'animeFormat',
385 | emptyText: 'Anime/{ ~plex.id }'
386 | }, {
387 | fieldLabel: 'Music Format',
388 | labelStyle: 'white-space: nowrap; width: 120px;',
389 | name: 'musicFormat',
390 | emptyText: '{ plex.id }'
391 | }, {
392 | fieldLabel: 'Unsorted Format',
393 | labelStyle: 'white-space: nowrap; width: 120px;',
394 | name: 'unsortedFormat',
395 | emptyText: 'Unsorted/{ relativeFile }'
396 | }]
397 | }, {
398 | title: 'Post Processing Options',
399 | defaults: {
400 | allowBlank: true,
401 | xtype: 'textfield',
402 | anchor: '100%'
403 | },
404 | items: [{
405 | xtype: 'checkboxfield',
406 | name: 'import',
407 | fieldLabel: 'Import Extras',
408 | boxLabel: 'copy companion files along from the original folder to the destination folder',
409 | checked: false
410 | }, {
411 | xtype: 'checkboxfield',
412 | name: 'metadata',
413 | fieldLabel: 'Export Xattr',
414 | boxLabel: 'copy xattr metadata into hidden .xattr folders',
415 | checked: false
416 | }, {
417 | xtype: 'checkboxfield',
418 | name: 'chmod',
419 | fieldLabel: 'Set Permissions',
420 | boxLabel: 'set permissions to all-readable / user-writable (rw-r--r--)',
421 | checked: false
422 | }, {
423 | name: 'apply',
424 | fieldLabel: 'Run Script',
425 | emptyText: '/path/to/apply.groovy'
426 | }, {
427 | name: 'exec',
428 | fieldLabel: 'Run Command',
429 | emptyText: 'stat {quote f}'
430 | }, {
431 | name: 'plex',
432 | fieldLabel: 'Plex',
433 | emptyText: 'host:token'
434 | }, {
435 | name: 'kodi',
436 | fieldLabel: 'Kodi',
437 | emptyText: 'host'
438 | }, {
439 | name: 'emby',
440 | fieldLabel: 'Emby',
441 | emptyText: 'host:apikey'
442 | }, {
443 | name: 'jellyfin',
444 | fieldLabel: 'Jellyfin',
445 | emptyText: 'host:apikey'
446 | }, {
447 | name: 'pushover',
448 | fieldLabel: 'Pushover',
449 | emptyText: 'userkey:apikey'
450 | }, {
451 | name: 'pushbullet',
452 | fieldLabel: 'PushBullet',
453 | emptyText: 'apikey'
454 | }, {
455 | name: 'discord',
456 | fieldLabel: 'Discord',
457 | emptyText: 'webhook'
458 | }, {
459 | xtype: 'combobox',
460 | name: 'report',
461 | fieldLabel: 'Report Folder',
462 | emptyText: '.reports',
463 | value: Ext.manifest.server.form.output,
464 | bind: {
465 | store: '{folders}'
466 | },
467 | displayField: 'path',
468 | valueField: 'path',
469 | minChars: 0, // forcing the query to run every time by setting minChars to 0
470 | queryCaching: true,
471 | queryParam: 'q',
472 | queryMode: 'remote',
473 | forceSelection: false,
474 | editable: true,
475 | anchor: '100%'
476 | }]
477 | }, {
478 | title: 'Database Options',
479 | defaults: {
480 | allowBlank: true,
481 | forceSelection: true,
482 | queryMode: 'local'
483 | },
484 | items: [{
485 | xtype: 'combobox',
486 | name: 'movieDB',
487 | fieldLabel: 'Movie Database',
488 | labelStyle: 'white-space: nowrap; width: 120px;',
489 | displayField: 'label',
490 | valueField: 'value',
491 | value: '',
492 | store: {
493 | type: 'movie-databases'
494 | },
495 | editable: false,
496 | minWidth: 320
497 | }, {
498 | xtype: 'combobox',
499 | name: 'seriesDB',
500 | fieldLabel: 'Series Database',
501 | labelStyle: 'white-space: nowrap; width: 120px;',
502 | displayField: 'label',
503 | valueField: 'value',
504 | value: '',
505 | store: {
506 | type: 'series-databases'
507 | },
508 | editable: false,
509 | minWidth: 320
510 | }, {
511 | xtype: 'combobox',
512 | name: 'animeDB',
513 | fieldLabel: 'Anime Database',
514 | labelStyle: 'white-space: nowrap; width: 120px;',
515 | displayField: 'label',
516 | valueField: 'value',
517 | value: '',
518 | store: {
519 | type: 'series-databases'
520 | },
521 | editable: false,
522 | minWidth: 320
523 | }]
524 | }, {
525 | title: 'Developer Options',
526 | defaults: {
527 | allowBlank: true,
528 | forceSelection: true,
529 | queryMode: 'local'
530 | },
531 | items: [{
532 | xtype: 'combobox',
533 | name: 'channel',
534 | fieldLabel: 'Script Channel',
535 | displayField: 'label',
536 | valueField: 'value',
537 | value: 'fn',
538 | store: {
539 | type: 'script-sources'
540 | },
541 | editable: false
542 | }, {
543 | xtype: 'combobox',
544 | name: 'log',
545 | fieldLabel: 'Log Level',
546 | displayField: 'label',
547 | valueField: 'value',
548 | value: 'all',
549 | store: {
550 | type: 'log-levels'
551 | },
552 | editable: false
553 | }, {
554 | xtype: 'checkboxfield',
555 | name: 'probe',
556 | fieldLabel: 'Media Parser',
557 | boxLabel: 'disable media parser',
558 | inputValue: 'no',
559 | checked: false
560 | }, {
561 | xtype: 'checkboxfield',
562 | name: 'index',
563 | fieldLabel: 'Media Index',
564 | boxLabel: 'disable media index',
565 | inputValue: 'no',
566 | checked: false
567 | }]
568 | }],
569 |
570 | buttons: [{
571 | xtype: 'button',
572 | scale: 'small',
573 | iconCls: 'configure-btn',
574 | text: 'Tools',
575 | menu: new Ext.menu.Menu({
576 | items: [
577 | // these will render as dropdown menu items when the arrow is clicked:
578 | { text: 'License', handler: 'onLicense', iconCls: 'license-item' },
579 | { xtype: 'menuseparator' },
580 | { text: 'Clear Cache', handler: 'onClear', iconCls: 'clear-item' },
581 | { text: 'System Info', handler: 'onInfo', iconCls: 'sysinfo-item' },
582 | { text: 'System Properties', handler: 'onSettings', iconCls: 'settings-item' },
583 | { text: 'Environment', handler: 'onEnvironment', iconCls: 'environment-item' },
584 | { text: 'Help', handler: 'onHelp', iconCls: 'help-item' },
585 | { xtype: 'menuseparator' },
586 | { text: 'MediaInfo', handler: 'onMediaInfo', iconCls: 'mediainfo-item' },
587 | { text: 'OpenSubtitles', handler: 'onConfigure', iconCls: 'configure-item' }
588 | ]
589 | }),
590 | width: 80,
591 | style: 'left: 6em !important' // align this button to the left
592 | }, {
593 | xtype: 'splitbutton',
594 | formBind: true,
595 | scale: 'small',
596 | iconCls: 'run-btn',
597 | text: 'Execute',
598 | // handle a click on the button itself
599 | handler: 'onExecute',
600 | menu: new Ext.menu.Menu({
601 | items: [
602 | // these will render as dropdown menu items when the arrow is clicked:
603 | {text: 'Dry Run', handler: 'onTest', iconCls: 'dryrun-item'},
604 | {text: 'Schedule', handler: 'onSchedule', iconCls: 'schedule-item'},
605 | {text: 'Revert', handler: 'onRevert', iconCls: 'revert-item' },
606 | ]
607 | }),
608 | width: 110,
609 | style:'margin-right: 3em'
610 | }]
611 | }, {
612 | xtype: 'container',
613 | region: 'south',
614 | frame: true,
615 | layout: 'border',
616 | height: 200,
617 | scrollable: false,
618 |
619 | items: [{
620 | region: 'west',
621 | xtype: 'taskmanager',
622 | headerPosition: 'left',
623 | collapsible: true,
624 | floatable: false,
625 | scrollable: 'vertical',
626 | width: 325
627 | }, {
628 | region: 'center',
629 | xtype: 'tasklogcat',
630 | collapsible: false,
631 | floatable: false,
632 | scrollable: true
633 | }]
634 | }]
635 | });
636 |
--------------------------------------------------------------------------------
/client-extjs/app/view/task/TaskController.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is the main view for the application. It is specified in app.js as the
3 | * "autoCreateViewport" property. That setting automatically applies the "viewport"
4 | * plugin to promote that instance of this class to the body element.
5 | *
6 | * TODO - Replace this content of this view to suite the needs of your application.
7 | */
8 | Ext.define('FileBot.view.task.TaskController', {
9 | extend: 'Ext.app.ViewController',
10 | requires: [
11 | 'FileBot.Node'
12 | ],
13 |
14 | alias: 'controller.task',
15 |
16 | /**
17 | * Called when the view is created
18 | */
19 | init: function() {
20 | FileBot.getApplication().on('init', function() {
21 | // start fetching folder data
22 | this.getViewModel().getStore('folders').setProxy(FileBot.Node.getDataProxy('folders'))
23 | }, this)
24 |
25 | FileBot.getApplication().on('auth', function(options) {
26 | var fields = this.getView().down('#media-server-options')
27 | if (options.auth == 'SYNO' || options.auth == 'QNAP') {
28 | // enable media server options by default
29 | fields.query('checkbox').forEach(function(checkbox) {
30 | checkbox.setValue(true)
31 | })
32 | fields.show()
33 | } else {
34 | // remove media server options from the form entirely
35 | fields.destroy()
36 | }
37 | }, this)
38 |
39 | FileBot.getApplication().on('state', function(json) {
40 | // restore form fields
41 | if (json) {
42 | var form = this.getForm()
43 | form.setValues(Ext.decode(json))
44 | form.isValid()
45 | }
46 | }, this)
47 |
48 | // show current environment
49 | FileBot.getApplication().on('environment', this.showEnvironmentForm, this)
50 | },
51 |
52 | restoreState: function() {
53 | var values = Ext.state.Manager.get('formOrganizeFiles')
54 | var form = this.getForm()
55 |
56 | // restore form values
57 | if (values) {
58 | form.setValues(values)
59 | }
60 |
61 | // mark invalid fields on init
62 | form.isValid()
63 | },
64 |
65 | saveState: function() {
66 | var values = this.getForm().getValues()
67 | Ext.state.Manager.set('formOrganizeFiles', values)
68 | FileBot.Node.requestState({'store': Ext.encode(values)})
69 | return values
70 | },
71 |
72 | onExecute: function() {
73 | var parameters = this.getExecuteParameters()
74 | if (parameters) {
75 | FileBot.Node.requestExecute(parameters)
76 | }
77 | },
78 |
79 | onTest: function() {
80 | var parameters = this.getExecuteParameters()
81 | if (parameters) {
82 | // force --action test and then execute normally
83 | parameters.action = 'TEST'
84 | FileBot.Node.requestExecute(parameters)
85 | }
86 | },
87 |
88 | onSchedule: function() {
89 | var parameters = this.getExecuteParameters()
90 | if (parameters) {
91 | FileBot.Node.requestSchedule(parameters)
92 | }
93 | },
94 |
95 | onMediaInfo: function() {
96 | var parameters = this.getExecuteParameters()
97 | if (parameters) {
98 | parameters.fn = 'mediainfo'
99 | FileBot.Node.requestExecute(parameters)
100 | }
101 | },
102 |
103 | onLicense: function() {
104 | Ext.create('Ext.window.Window', {
105 | id: 'licenseWindow',
106 | items: [{
107 | xtype: 'form',
108 | id: 'licenseForm',
109 | items: [{
110 | xtype: 'hidden',
111 | name: 'fn',
112 | value: 'license'
113 | }, {
114 | xtype: 'textareafield',
115 | width: 540,
116 | height: 360,
117 | id: 'licenseTextArea',
118 | name: 'license',
119 | fieldCls: 'license',
120 | allowBlank: false,
121 | emptyText: '-----BEGIN PGP SIGNED MESSAGE-----\n\n\n\n\n\n\n\n\n-----BEGIN PGP SIGNATURE-----\n\n\n\n\n\n\n\n\n\n-----END PGP SIGNATURE-----'
122 | }],
123 | buttons: [
124 | {
125 | xtype: 'filefield',
126 | width: 75,
127 | buttonOnly: true,
128 | accept: '.psm',
129 | buttonConfig: {
130 | text: 'Select',
131 | iconCls: 'select-btn'
132 | },
133 | listeners: {
134 | change: function(evt) {
135 | var file = evt.fileInputEl.dom.files[0]
136 | var reader = new FileReader()
137 | reader.onload = function(evt) {
138 | Ext.getCmp('licenseTextArea').setValue(evt.target.result)
139 | }
140 | reader.readAsText(file)
141 | }
142 | }
143 | },
144 | { xtype: 'tbfill' },
145 | { text:'Purchase', iconCls: 'purchase-btn', handler: function(btn) {
146 | window.open(Ext.manifest.server.url.license_purchase, '_blank')
147 | }},
148 | { text:'Activate', iconCls: 'license-btn', formBind: true, handler: function(btn) {
149 | var form = Ext.getCmp('licenseForm').getForm()
150 | if (form.isValid()) {
151 | FileBot.Node.requestExecute(form.getValues())
152 | Ext.getCmp('licenseWindow').destroy()
153 | }
154 | }}
155 | ],
156 | }],
157 | title: 'Activate License',
158 | bodyPadding: 10,
159 | scrollable: false,
160 | resizable: false,
161 | closable: true
162 | }).show()
163 | },
164 |
165 | onConfigure: function() {
166 | Ext.create('Ext.window.Window', {
167 | id: 'osdbWindow',
168 | items: [{
169 | xtype: 'form',
170 | id: 'osdbForm',
171 | items: [{
172 | xtype: 'hidden',
173 | name: 'fn',
174 | value: 'configure'
175 | }, {
176 | xtype: 'textfield',
177 | allowBlank: false,
178 | fieldLabel: 'Username',
179 | name: 'osdbUser',
180 | emptyText: 'username'
181 | }, {
182 | xtype: 'textfield',
183 | allowBlank: false,
184 | fieldLabel: 'Password',
185 | name: 'osdbPwd',
186 | emptyText: 'password',
187 | inputType: 'password'
188 | }],
189 | buttons: [
190 | { text:'Register', handler: function(btn) {
191 | window.open(Ext.manifest.server.url.osdb_register, '_blank')
192 | }},
193 | { text:'Login', formBind: true, handler: function(btn) {
194 | var form = Ext.getCmp('osdbForm').getForm()
195 | if (form.isValid()) {
196 | FileBot.Node.requestExecute(form.getValues())
197 | Ext.getCmp('osdbWindow').destroy()
198 | }
199 | }}
200 | ],
201 | }],
202 | title: 'OpenSubtitles',
203 | bodyPadding: 10,
204 | scrollable: false,
205 | resizable: false,
206 | closable: true
207 | }).show()
208 | },
209 |
210 | onSettings: function() {
211 | Ext.create('Ext.window.Window', {
212 | id: 'settingsWindow',
213 | items: [{
214 | xtype: 'form',
215 | id: 'settingsForm',
216 | items: [{
217 | xtype: 'hidden',
218 | name: 'fn',
219 | value: 'properties'
220 | }, {
221 | xtype: 'textfield',
222 | allowBlank: false,
223 | fieldLabel: 'Name',
224 | name: 'name',
225 | emptyText: 'net.filebot.xattr.store'
226 | }, {
227 | xtype: 'textfield',
228 | allowBlank: true,
229 | fieldLabel: 'Value',
230 | name: 'value',
231 | emptyText: '.xattr'
232 | }],
233 | buttons: [
234 | { text:'Set', formBind: true, handler: function(btn) {
235 | var form = Ext.getCmp('settingsForm').getForm()
236 | if (form.isValid()) {
237 | FileBot.Node.requestExecute(form.getValues())
238 | Ext.getCmp('settingsWindow').destroy()
239 | }
240 | }}
241 | ],
242 | }],
243 | title: 'System Properties',
244 | bodyPadding: 10,
245 | scrollable: false,
246 | resizable: false,
247 | closable: true
248 | }).show()
249 | },
250 |
251 | onRevert: function() {
252 | var parameters = {'fn':'revert'}
253 | FileBot.Node.requestExecute(parameters)
254 | },
255 |
256 | onInfo: function() {
257 | var parameters = {'fn':'sysinfo'}
258 | FileBot.Node.requestExecute(parameters)
259 | },
260 |
261 | onClear: function() {
262 | var parameters = {'fn':'clear'}
263 | FileBot.Node.requestExecute(parameters)
264 | },
265 |
266 | onEnvironment: function() {
267 | var parameters = {}
268 | FileBot.Node.requestEnvironment(parameters)
269 | },
270 |
271 | onHelp: function() {
272 | window.open(Ext.manifest.server.url.help, '_blank')
273 | },
274 |
275 | getExecuteParameters: function() {
276 | var form = this.getForm()
277 | if (form.isValid()) {
278 | return this.saveState()
279 | }
280 | return null
281 | },
282 |
283 | getForm: function() {
284 | return this.getView().down('form').getForm()
285 | },
286 |
287 | showEnvironmentForm: function(data) {
288 | // show status message
289 | if (data.message != null) {
290 | Ext.create('Ext.window.MessageBox', {
291 | // set closeAction to 'destroy' if this instance is not
292 | // intended to be reused by the application
293 | closeAction: 'destroy'
294 | }).show({
295 | title: 'Environment',
296 | msg: data.message,
297 | buttons: Ext.MessageBox.OK,
298 | icon: Ext.MessageBox.INFO
299 | })
300 | }
301 |
302 | // show environment input form
303 | if (data.environment != null) {
304 | Ext.create('Ext.window.Window', {
305 | id: 'environmentWindow',
306 | items: [{
307 | xtype: 'form',
308 | id: 'environmentForm',
309 | items: [{
310 | xtype: 'textareafield',
311 | width: 540,
312 | height: 360,
313 | id: 'environmentTextArea',
314 | name: 'environment',
315 | fieldCls: 'environment',
316 | allowBlank: true,
317 | value: data.environment,
318 | emptyText: 'export JAVA_OPTS=-Xmx512m'
319 | }],
320 | buttons: [
321 | { text:'Set Environment', formBind: true, handler: function(btn) {
322 | var environment = Ext.getCmp('environmentForm').getForm().getValues()['environment']
323 | FileBot.Node.requestEnvironment({'environment': environment})
324 | Ext.getCmp('environmentWindow').destroy()
325 | }}
326 | ],
327 | }],
328 | title: 'Environment',
329 | bodyPadding: 10,
330 | scrollable: false,
331 | resizable: false,
332 | closable: true
333 | }).show()
334 | }
335 | }
336 |
337 | });
338 |
--------------------------------------------------------------------------------
/client-extjs/app/view/task/TaskModel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is the view model for the Main view of the application.
3 | */
4 | Ext.define('FileBot.view.task.TaskModel', {
5 | extend: 'Ext.app.ViewModel',
6 |
7 | alias: 'viewmodel.task',
8 |
9 | stores: {
10 | folders: {
11 | storeId: 'folders-store',
12 | autoLoad: false,
13 | pageSize: 0,
14 | remoteFilter: false,
15 | remoteSort: false,
16 |
17 | fields: [
18 | { name: 'path', type: 'string' }
19 | ]
20 | }
21 | },
22 |
23 | data: {
24 | name: 'FileBot'
25 | }
26 |
27 | });
28 |
--------------------------------------------------------------------------------
/client-extjs/app/view/tasklogcat/TaskLogCat.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by reinhard on 4/29/15.
3 | */
4 | Ext.define('FileBot.view.tasklogcat.TaskLogCat', {
5 | extend: 'Ext.panel.Panel',
6 | requires: [
7 | 'FileBot.view.tasklogcat.TaskLogCatController'
8 | ],
9 | xtype: 'tasklogcat',
10 |
11 | viewModel: {
12 | type: 'tasklogcat'
13 | },
14 | controller: 'tasklogcat',
15 |
16 | layout: {
17 | type: 'hbox',
18 | align: 'stretch'
19 | },
20 | frame: false,
21 | autoScroll: true,
22 | autoWidth: true,
23 | scrollable: true,
24 | focusable: false,
25 | editable: false,
26 | flex: 1,
27 | border: 0,
28 |
29 | items: [{
30 | xtype: 'textarea',
31 | id: 'logcatviewer',
32 | fieldCls: 'logcatviewer',
33 | emptyText: '$',
34 | scrollable: false,
35 | focusable: false,
36 | editable: false,
37 | flex: 1,
38 | border: 0
39 | }]
40 |
41 | });
42 |
--------------------------------------------------------------------------------
/client-extjs/app/view/tasklogcat/TaskLogCatController.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by reinhard on 4/29/15.
3 | */
4 | Ext.define('FileBot.view.tasklogcat.TaskLogCatController', {
5 | extend: 'Ext.app.ViewController',
6 | requires: [
7 | 'FileBot.view.tasklogcat.TaskLogCatModel',
8 | 'Ext.util.TaskManager',
9 | 'FileBot.Node'
10 | ],
11 | alias: 'controller.tasklogcat',
12 |
13 | // task that is corrently locked on for log viewing
14 | task: null,
15 |
16 | // run this task repeatedly until process has finished producing output
17 | refreshJob: null,
18 |
19 | init: function() {
20 | this.refreshJob = Ext.util.TaskManager.newTask({
21 | run: this.refresh,
22 | interval: Ext.manifest.server.refresh,
23 | scope: this
24 | })
25 |
26 | // watch log of the newly selected task
27 | FileBot.getApplication().on('selectTask', function(record) {
28 | // stop existing refresh job if any
29 | this.refreshJob.stop()
30 |
31 | this.task = record
32 | this.refresh()
33 |
34 | // if task has not completed yet keep watching for new output
35 | if (this.task.status == '') {
36 | this.refreshJob.start()
37 | }
38 | }, this)
39 |
40 | FileBot.getApplication().on('version', function(message) {
41 | var val = ['$ filebot -version', message].join('\n')
42 | var cmp = Ext.getCmp('logcatviewer')
43 | cmp.setValue(val)
44 | }, this)
45 | },
46 |
47 | refresh: function() {
48 | // fetch new log and update textarea
49 | FileBot.Node.fetchLog(this.task, function(response) {
50 | var val = response.responseText
51 | var cmp = Ext.getCmp('logcatviewer')
52 |
53 | // detect blocked requests
54 | if (!val) {
55 | console.log('Invalid Response', response)
56 | val = 'Invalid Response'
57 | + '\n└─ url: ' + JSON.stringify(response.request.requestOptions.url)
58 | + '\n└─ status: ' + JSON.stringify(response.status)
59 | + '\n└─ response: ' + JSON.stringify(response.responseText)
60 | + '\n\nPlease disable your Ad Blocker. Check Inspect ➔ Console / Network for details.'
61 | }
62 |
63 | if (val != cmp.getValue()) {
64 | cmp.setValue(val)
65 |
66 | // stop checking for updates once the task is done
67 | if (val.match(/\[Process (?:completed|error|killed)\]/g)) {
68 | this.refreshJob.stop()
69 | }
70 | }
71 | }.bind(this))
72 | }
73 |
74 | });
75 |
--------------------------------------------------------------------------------
/client-extjs/app/view/tasklogcat/TaskLogCatModel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by reinhard on 4/29/15.
3 | */
4 | Ext.define('FileBot.view.tasklogcat.TaskLogCatModel', {
5 | extend: 'Ext.app.ViewModel',
6 | alias: 'viewmodel.tasklogcat',
7 |
8 | stores: {
9 | /*
10 | A declaration of Ext.data.Store configurations that are first processed as binds to produce an effective
11 | store configuration. For example:
12 |
13 | users: {
14 | model: 'TaskLogCat',
15 | autoLoad: true
16 | }
17 | */
18 | },
19 |
20 | data: {
21 | /* This object holds the arbitrary data that populates the ViewModel and is then available for binding. */
22 | }
23 | });
24 |
--------------------------------------------------------------------------------
/client-extjs/app/view/taskmanager/TaskManager.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by reinhard on 4/25/15.
3 | */
4 | Ext.define('FileBot.view.taskmanager.TaskManager', {
5 | extend: 'Ext.grid.Panel',
6 | requires: [
7 | 'FileBot.view.taskmanager.TaskManagerController',
8 | 'FileBot.view.taskmanager.TaskManagerModel',
9 | 'FileBot.Node'
10 | ],
11 | viewModel: {
12 | type: 'taskmanager'
13 | },
14 | controller: 'taskmanager',
15 | xtype: 'taskmanager',
16 |
17 | bind: '{tasks}',
18 |
19 | tools: [{
20 | type: 'print',
21 | callback: function() {
22 | FileBot.Node.openEndpoint('output', {})
23 | }
24 | }],
25 |
26 | listeners: {
27 | select: function(view, record) {
28 | // broadcast event
29 | FileBot.getApplication().fireEvent('selectTask', record.data)
30 | }
31 | },
32 |
33 | columns: [
34 | {
35 | text: 'Date',
36 | dataIndex: 'date',
37 | width: 120,
38 | renderer: function(val) {
39 | var t = new Date(val)
40 | var date = Ext.util.Format.date(t, 'd M')
41 | var time = Ext.util.Format.date(t, 'H:i:s')
42 | return ''+date+''+''+time+''
43 | }
44 | }, {
45 | text: 'Status',
46 | dataIndex: 'status',
47 | width: 100,
48 | align: 'left',
49 | renderer: function(val) {
50 | if (val == '')
51 | return 'Running'
52 | if (val == '0')
53 | return 'Complete'
54 | if (val == '100')
55 | return 'Complete' // NOOP
56 | if (val == '137')
57 | return 'Cancelled'
58 | if (val == '1000')
59 | return 'Scheduled'
60 | else
61 | return 'Failure'
62 | }
63 | }, {
64 | menuDisabled: true,
65 | sortable: false,
66 | xtype: 'actioncolumn',
67 | focusable: false,
68 | width: 50,
69 | align: 'center',
70 | items: [{
71 | getClass: function(v, meta, rec) {
72 | var val = rec.get('status')
73 | if (val == '')
74 | return 'cancel-col'
75 | if (val == '0')
76 | return 'ok-col'
77 | if (val == '100')
78 | return 'ok-col' // NOOP
79 | if (val == '1000')
80 | return 'schedule-col'
81 | else
82 | return 'fail-col'
83 | },
84 | getTip: function(v, meta, rec) {
85 | var val = rec.get('status')
86 | if (val == '')
87 | return 'Cancel'
88 | if (val == '0')
89 | return 'Success'
90 | if (val == '100')
91 | return 'No Operation'
92 | if (val == '137')
93 | return 'Cancelled'
94 | if (val == '1000')
95 | return 'Execute Task'
96 | else
97 | return 'Error (' + val + ')'
98 | },
99 | handler: function(grid, rowIndex, colIndex) {
100 | var rec = grid.getStore().getAt(rowIndex)
101 | var val = rec.get('status')
102 | if (val == '') {
103 | FileBot.Node.requestKill({id: rec.get('id')})
104 | }
105 | else if (val == '1000') {
106 | FileBot.Node.openEndpoint("task", {id: rec.get('id')})
107 | }
108 | }
109 | }]
110 | }],
111 |
112 | title: 'Tasks',
113 | sortableColumns: false,
114 | enableColumnHide: false,
115 | enableColumnMove: false,
116 |
117 | viewConfig: {
118 | enableTextSelection: false,
119 | deferEmptyText: false,
120 | loadMask: false
121 | }
122 | });
123 |
--------------------------------------------------------------------------------
/client-extjs/app/view/taskmanager/TaskManagerController.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by reinhard on 4/25/15.
3 | */
4 | Ext.define('FileBot.view.taskmanager.TaskManagerController', {
5 | extend: 'Ext.app.ViewController',
6 | requires: [
7 | 'FileBot.view.taskmanager.TaskManagerModel',
8 | 'Ext.util.TaskManager'
9 | ],
10 | alias: 'controller.taskmanager',
11 |
12 | /**
13 | * Called when the view is created
14 | */
15 | init: function() {
16 | const store = this.getViewModel().getStore('tasks')
17 |
18 | FileBot.getApplication().on('init', function() {
19 | // start fetching task data
20 | store.setProxy(FileBot.Node.getDataProxy('tasks'))
21 | // refresh task state every few seconds
22 | Ext.util.TaskManager.start({
23 | run: store.reload,
24 | interval: Ext.manifest.server.refresh,
25 | scope: store
26 | })
27 | }, this)
28 |
29 | // immediately refresh data when new tasks are executed
30 | FileBot.getApplication().on('execute', function() {
31 | // auto-select first row after new rows have been loaded and rendered
32 | this.selectFirstRowOnUpdate = true
33 | store.reload()
34 | }, this)
35 |
36 | // same for filebot --license calls
37 | FileBot.getApplication().on('license', function() {
38 | // auto-select first row after new rows have been loaded and rendered
39 | this.selectFirstRowOnUpdate = true
40 | store.reload()
41 | }, this)
42 |
43 | // auto-select newly added tasks (on 'add' event doesn't work for grid)
44 | store.on('datachanged', this.updateFirstRowSelection, this)
45 | },
46 |
47 |
48 | selectFirstRowOnUpdate: false,
49 | updateFirstRowSelection: function() {
50 | if (this.selectFirstRowOnUpdate) {
51 | this.selectFirstRowOnUpdate = false
52 | this.getView().getSelectionModel().select(0)
53 | }
54 | }
55 |
56 | });
57 |
--------------------------------------------------------------------------------
/client-extjs/app/view/taskmanager/TaskManagerModel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by reinhard on 4/25/15.
3 | */
4 | Ext.define('FileBot.view.taskmanager.TaskManagerModel', {
5 | extend: 'Ext.app.ViewModel',
6 | requires: [
7 | 'FileBot.Node'
8 | ],
9 | alias: 'viewmodel.taskmanager',
10 |
11 | stores: {
12 | tasks: {
13 | storeId: 'tasks-store',
14 | autoLoad: false,
15 | pageSize: 0,
16 | remoteFilter: false,
17 | remoteSort: false,
18 |
19 | fields: [
20 | { name: 'id', type: 'string' },
21 | { name: 'date', type: 'int' },
22 | { name: 'status', type: 'string' }
23 | ],
24 |
25 | sorters: [{
26 | property: 'date',
27 | direction: 'DESC'
28 | }]
29 | }
30 | },
31 |
32 | data: {
33 | /* This object holds the arbitrary data that populates the ViewModel and is then available for binding. */
34 | }
35 | });
36 |
--------------------------------------------------------------------------------
/client-extjs/build.xml:
--------------------------------------------------------------------------------
1 |
2 |
NOTE
18 |FileBot Node cannot access your files unless you explicitly grant Read/Write permissions.
19 |FileBot Node requires Node.js. Please install Node.js first.
' >> "$SYNOPKG_TEMP_LOGFILE" 7 | exit 1 8 | fi 9 | 10 | 11 | # require filebot 12 | if ! which filebot; then 13 | echo 'FileBot Node requires FileBot. Please install FileBot first.
' >> "$SYNOPKG_TEMP_LOGFILE" 14 | exit 1 15 | fi 16 | 17 | 18 | # nginx reverse proxy configuration installed by FileBot Node pre-DSM 6.2.4 (as default root user) 19 | # can no longer be uninstalled by FileBot Node after DSM 6.2.4 (as new default system user) 20 | if [ -f '/usr/local/etc/nginx/conf.d/dsm.filebot-node.conf' ]; then 21 | { 22 | echo 'Please use sudo to remove dsm.filebot-node.conf manually before installing FileBot Node:
' 23 | echo 'sudo rm -v /usr/local/etc/nginx/conf.d/dsm.filebot-node.conf
' 24 | echo '* The nginx reverse proxy configuration file installed by FileBot Node pre-DSM 6.2.4 (as default root user) can no longer be uninstalled by FileBot Node after DSM 6.2.4 (as new default system user) because the system user does not have root permissions.
' 25 | } >> "$SYNOPKG_TEMP_LOGFILE" 26 | exit 1 27 | fi 28 | 29 | 30 | # return successfully 31 | exit 0 32 | -------------------------------------------------------------------------------- /package/synology-dsm7/scripts/preuninst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exit 0 3 | -------------------------------------------------------------------------------- /package/synology-dsm7/scripts/preupgrade: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exit 0 3 | -------------------------------------------------------------------------------- /package/synology-dsm7/scripts/start-stop-status: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | 4 | case "$1" in 5 | start) 6 | "$SYNOPKG_PKGDEST/bin/filebot-node-start" >> "$SYNOPKG_PKGVAR/filebot-node.log" 2>&1 & 7 | exit $? 8 | ;; 9 | 10 | stop) 11 | killall -u "FileBot" -- "node" >> "$SYNOPKG_PKGVAR/filebot-node.log" 2>&1 12 | exit $? 13 | ;; 14 | 15 | status) 16 | ps -u "FileBot" | grep "node" 17 | exit $? 18 | ;; 19 | esac 20 | -------------------------------------------------------------------------------- /package/synology-dsm7/target/bin/filebot-node-start: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | 4 | echo " 5 | -------------------- Run $0 (PID: $$) -------------------- $(date) 6 | " 7 | 8 | 9 | export FILEBOT_NODE_HOST="127.0.0.1" # bind to local host 10 | export FILEBOT_NODE_AUTH="SYNO" 11 | 12 | export FILEBOT_NODE_HTTP="YES" 13 | export FILEBOT_NODE_HTTP_PORT="5452" 14 | 15 | export FILEBOT_NODE_DATA="/var/packages/filebot-node/var" 16 | 17 | export FILEBOT_TASK_CMD="filebot-node-task" 18 | 19 | export FILEBOT_CMD="filebot" 20 | export FILEBOT_CMD_CWD="$SYNOPKG_PKGDEST_VOL" 21 | 22 | export FILEBOT_CMD_UID=$(id -u) 23 | export FILEBOT_CMD_GID=$(id -g) 24 | 25 | # set working dir 26 | cd "/var/packages/filebot-node/target" 27 | 28 | 29 | # import user environment 30 | . "$FILEBOT_NODE_DATA/environment.sh" 31 | 32 | 33 | exec node "server/app.js" 34 | -------------------------------------------------------------------------------- /package/synology-dsm7/target/bin/filebot-node-task: -------------------------------------------------------------------------------- 1 | #!/bin/bash -u 2 | 3 | export FILEBOT_NODE_DATA="/var/packages/filebot-node/var" 4 | export FILEBOT_NODE_TASK="$1" 5 | 6 | 7 | # sanity check 8 | if [ ! -f "$FILEBOT_NODE_DATA/task/$FILEBOT_NODE_TASK.args" ]; then 9 | echo "$0: Task $FILEBOT_NODE_TASK does not exist" 10 | exit 1 11 | fi 12 | 13 | 14 | # import user environment 15 | . "$FILEBOT_NODE_DATA/environment.sh" 16 | 17 | 18 | # execute filebot task and record output 19 | filebot "@$FILEBOT_NODE_DATA/task/$FILEBOT_NODE_TASK.args" 2>&1 | tee -a "$FILEBOT_NODE_DATA/log/$FILEBOT_NODE_TASK.log" 20 | 21 | 22 | # get filebot exit code (and not tee exit code) 23 | STATUS="${PIPESTATUS[0]}" 24 | 25 | # treat ExitCode.NOOP as ExitCode.SUCCESS 26 | if [ $STATUS -eq 100 ]; then 27 | exit 0 28 | fi 29 | 30 | exit $STATUS 31 | -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/FileBot.NodeClient.js: -------------------------------------------------------------------------------- 1 | // Namespace definition 2 | Ext.ns("FileBot.NodeClient"); 3 | 4 | // Application definition 5 | Ext.define("FileBot.NodeClient.AppInstance", { 6 | extend: "SYNO.SDS.AppInstance", 7 | appWindowName: "FileBot.NodeClient.AppWindow" 8 | }); 9 | 10 | // Window definition 11 | Ext.define("FileBot.NodeClient.AppWindow", { 12 | extend: "SYNO.SDS.AppWindow", 13 | 14 | constructor: function(config) { 15 | this.appInstance = config.appInstance; 16 | 17 | config = Ext.apply({ 18 | resizable: true, 19 | maximizable: true, 20 | minimizable: true, 21 | width: 980, 22 | height: 580, 23 | minWidth: 830, 24 | minHeight: 510, 25 | items: [{ 26 | xtype: 'box', 27 | autoEl: { 28 | tag: 'iframe', 29 | src: '/webman/3rdparty/filebot-node/index.html', 30 | width: '100%', 31 | height: '100%', 32 | frameborder: '0' 33 | } 34 | }], 35 | tools: [{ 36 | id: 'fullscreen', 37 | qtip: 'Open in New Tab', 38 | handler: function(event, element, panel) { 39 | window.open('/webman/3rdparty/filebot-node/index.html', '_blank') 40 | } 41 | }, { 42 | id: 'help', 43 | qtip: 'Open Help', 44 | handler: function(event, element, panel) { 45 | window.open('https://www.filebot.net/node.html', '_blank') 46 | } 47 | }] 48 | }, config); 49 | 50 | this.callParent([config]); 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/auth: -------------------------------------------------------------------------------- 1 | {"success":true,"data":{"auth":"SYNO"}} -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/config: -------------------------------------------------------------------------------- 1 | { 2 | "FileBot.NodeClient.js": { 3 | "FileBot.NodeClient.AppInstance": { 4 | "type": "app", 5 | "title": "app:name", 6 | "desc": "app:description", 7 | "icon": "images/filebot_node_{0}.png", 8 | "texts": "texts", 9 | "allowMultiInstance": false, 10 | "allUsers": true, 11 | "appWindow": "FileBot.NodeClient.AppWindow", 12 | "depend": ["FileBot.NodeClient.AppWindow"] 13 | }, 14 | "FileBot.NodeClient.AppWindow": { 15 | "type": "lib", 16 | "title": "app:name", 17 | "icon": "images/filebot_node_{0}.png", 18 | "texts": "texts" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/environment.cgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . /usr/syno/synoman/webman/3rdparty/filebot-node/proxy_pass.cgi 3 | -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/execute.cgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . /usr/syno/synoman/webman/3rdparty/filebot-node/proxy_pass.cgi 3 | -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/folders.cgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . /usr/syno/synoman/webman/3rdparty/filebot-node/proxy_pass.cgi 3 | -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/help/enu/filebot_node_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | FileBot Node for Synology NAS 13 | 14 | -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/helptoc.conf: -------------------------------------------------------------------------------- 1 | { 2 | "app": "FileBot.NodeClient.AppInstance", 3 | "title": "app:name", 4 | "content": "filebot_node_index.html", 5 | "helpset": "help", 6 | "stringset": "texts" 7 | } 8 | -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/images/filebot_node_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebot/filebot-node/f1ecfa0d9a16ec0598aeb141e04cf4dd87b0f6ae/package/synology-dsm7/target/client/images/filebot_node_16.png -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/images/filebot_node_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebot/filebot-node/f1ecfa0d9a16ec0598aeb141e04cf4dd87b0f6ae/package/synology-dsm7/target/client/images/filebot_node_24.png -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/images/filebot_node_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebot/filebot-node/f1ecfa0d9a16ec0598aeb141e04cf4dd87b0f6ae/package/synology-dsm7/target/client/images/filebot_node_256.png -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/images/filebot_node_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebot/filebot-node/f1ecfa0d9a16ec0598aeb141e04cf4dd87b0f6ae/package/synology-dsm7/target/client/images/filebot_node_32.png -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/images/filebot_node_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebot/filebot-node/f1ecfa0d9a16ec0598aeb141e04cf4dd87b0f6ae/package/synology-dsm7/target/client/images/filebot_node_48.png -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/images/filebot_node_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebot/filebot-node/f1ecfa0d9a16ec0598aeb141e04cf4dd87b0f6ae/package/synology-dsm7/target/client/images/filebot_node_64.png -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/images/filebot_node_72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebot/filebot-node/f1ecfa0d9a16ec0598aeb141e04cf4dd87b0f6ae/package/synology-dsm7/target/client/images/filebot_node_72.png -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/kill.cgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . /usr/syno/synoman/webman/3rdparty/filebot-node/proxy_pass.cgi 3 | -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/output.cgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . /usr/syno/synoman/webman/3rdparty/filebot-node/proxy_pass.cgi 3 | -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/proxy_pass.cgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PROXY_PASS='http://127.0.0.1:5452' 4 | PROXY_FILE="$(basename "$SCRIPT_NAME" '.cgi')" 5 | 6 | exec -c -a 'proxy_pass.cgi' \ 7 | curl \ 8 | --include \ 9 | --silent \ 10 | --no-buffer \ 11 | --header "If-Modified-Since: $HTTP_IF_MODIFIED_SINCE" \ 12 | --header "X-Syno-Token: $HTTP_X_SYNO_TOKEN" \ 13 | --header "X-Real-IP: $REMOTE_ADDR" \ 14 | --cookie "$HTTP_COOKIE" \ 15 | "$PROXY_PASS/$PROXY_FILE?$QUERY_STRING" 16 | -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/schedule.cgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . /usr/syno/synoman/webman/3rdparty/filebot-node/proxy_pass.cgi 3 | -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/state.cgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . /usr/syno/synoman/webman/3rdparty/filebot-node/proxy_pass.cgi 3 | -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/task.cgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . /usr/syno/synoman/webman/3rdparty/filebot-node/proxy_pass.cgi 3 | -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/tasks.cgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . /usr/syno/synoman/webman/3rdparty/filebot-node/proxy_pass.cgi 3 | -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/test.cgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo 'Status: 200 OK' 4 | echo 'Content-Type: text/plain; charset=UTF-8' 5 | echo '' 6 | 7 | if /usr/syno/synoman/webman/authenticate.cgi; then 8 | echo '---------- printenv ----------' 9 | printenv 10 | echo '---------- id ----------' 11 | id 12 | echo '---------- node ----------' 13 | node -v 2>&1 14 | echo '---------- java ----------' 15 | java -version 2>&1 16 | echo '---------- filebot ----------' 17 | filebot -script fn:sysinfo 2>&1 18 | echo '---------- filebot-node ----------' 19 | cat /var/packages/filebot-node/var/filebot-node.log 20 | else 21 | echo $? 22 | fi 23 | -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/texts/enu/strings: -------------------------------------------------------------------------------- 1 | [app] 2 | name = "FileBot Node" 3 | description = "FileBot Node allows you to execute, monitor and schedule filebot commands." 4 | -------------------------------------------------------------------------------- /package/synology-dsm7/target/client/version.cgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . /usr/syno/synoman/webman/3rdparty/filebot-node/proxy_pass.cgi 3 | -------------------------------------------------------------------------------- /package/synology/conf/privilege: -------------------------------------------------------------------------------- 1 | { 2 | "defaults":{ 3 | "run-as": "system" 4 | }, 5 | "username": "FileBot", 6 | "groupname": "FileBot" 7 | } 8 | -------------------------------------------------------------------------------- /package/synology/conf/resource: -------------------------------------------------------------------------------- 1 | { 2 | "usr-local-linker": { 3 | "bin": ["bin/filebot-node-task"] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /package/synology/scripts/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | FILEBOT_NODE_DATA="/var/packages/filebot-node/target/data" 3 | 4 | # prepare application data folder with read / write permissions for all users 5 | mkdir "$FILEBOT_NODE_DATA" 6 | 7 | # prepare log file permissions 8 | touch "$FILEBOT_NODE_DATA/filebot.log" 9 | chmod 666 "$FILEBOT_NODE_DATA/filebot.log" 10 | 11 | # set -Xmx to 0.8 of physical memory (on low-memory devices) 12 | if [ ! -f "$FILEBOT_NODE_DATA/environment.sh" ]; then 13 | JAVA_OPTS=$(awk '/MemTotal:/ { xmx = ($2*0.8)/1024; if (xmx < 1024) { printf "-Xmx%dm", xmx }; exit}' /proc/meminfo) 14 | echo "export JAVA_OPTS=$JAVA_OPTS" > "$FILEBOT_NODE_DATA/environment.sh" 15 | fi 16 | 17 | # print notification 18 | { 19 | cat << EOF 20 |NOTE
21 |FileBot Node cannot access your files unless you explicitly grant Read/Write permissions.
22 |FileBot Node requires Node.js. Please install Node.js first.
' >> "$SYNOPKG_TEMP_LOGFILE" 7 | exit 1 8 | fi 9 | 10 | 11 | # require filebot 12 | if ! which filebot; then 13 | echo 'FileBot Node requires FileBot. Please install FileBot first.
' >> "$SYNOPKG_TEMP_LOGFILE" 14 | exit 1 15 | fi 16 | 17 | 18 | # nginx reverse proxy configuration installed by FileBot Node pre-DSM 6.2.4 (as default root user) 19 | # can no longer be uninstalled by FileBot Node after DSM 6.2.4 (as new default system user) 20 | if [ -f '/usr/local/etc/nginx/conf.d/dsm.filebot-node.conf' ]; then 21 | { 22 | echo 'Please use sudo to remove dsm.filebot-node.conf manually before installing FileBot Node:
' 23 | echo 'sudo rm -v /usr/local/etc/nginx/conf.d/dsm.filebot-node.conf
' 24 | echo '* The nginx reverse proxy configuration file installed by FileBot Node pre-DSM 6.2.4 (as default root user) can no longer be uninstalled by FileBot Node after DSM 6.2.4 (as new default system user) because the system user does not have root permissions.
' 25 | } >> "$SYNOPKG_TEMP_LOGFILE" 26 | exit 1 27 | fi 28 | 29 | 30 | # return successfully 31 | exit 0 32 | -------------------------------------------------------------------------------- /package/synology/scripts/preuninst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exit 0 3 | -------------------------------------------------------------------------------- /package/synology/scripts/preupgrade: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # store data files 4 | cd "$SYNOPKG_PKGDEST" && tar -cvzf "/tmp/$SYNOPKG_PKGNAME.data.tgz" "data/" 5 | 6 | # return successfully 7 | exit 0 8 | -------------------------------------------------------------------------------- /package/synology/scripts/start-stop-status: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | 4 | case "$1" in 5 | start) 6 | "$SYNOPKG_PKGDEST/bin/filebot-node-start" >> "$SYNOPKG_PKGDEST/filebot-node.log" 2>&1 & 7 | exit $? 8 | ;; 9 | 10 | stop) 11 | kill "$("$0" status)" >> "$SYNOPKG_PKGDEST/filebot-node.log" 2>&1 12 | exit $? 13 | ;; 14 | 15 | status) 16 | curl -fs 'http://127.0.0.1:5452/status' | grep -oE '"pid":[0-9]+' | grep -oE '[0-9]+' 17 | exit $? 18 | ;; 19 | esac 20 | -------------------------------------------------------------------------------- /package/synology/target/bin/filebot-node-start: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | 4 | echo " 5 | -------------------- Run $0 (PID: $$) -------------------- $(date) 6 | " 7 | 8 | 9 | export FILEBOT_NODE_HOST="127.0.0.1" # bind to local host 10 | export FILEBOT_NODE_AUTH="SYNO" 11 | 12 | export FILEBOT_NODE_HTTP="YES" 13 | export FILEBOT_NODE_HTTP_PORT="5452" 14 | 15 | export FILEBOT_NODE_DATA="/var/packages/filebot-node/target/data" 16 | 17 | export FILEBOT_TASK_CMD="filebot-node-task" 18 | 19 | export FILEBOT_CMD="filebot" 20 | export FILEBOT_CMD_CWD="$SYNOPKG_PKGDEST_VOL" 21 | 22 | export FILEBOT_CMD_UID=$(id -u FileBot) 23 | export FILEBOT_CMD_GID=$(id -g FileBot) 24 | 25 | # set working dir 26 | cd "/var/packages/filebot-node/target" 27 | 28 | 29 | # import user environment 30 | . "$FILEBOT_NODE_DATA/environment.sh" 31 | 32 | 33 | exec node "server/app.js" 34 | -------------------------------------------------------------------------------- /package/synology/target/bin/filebot-node-task: -------------------------------------------------------------------------------- 1 | #!/bin/bash -u 2 | 3 | export FILEBOT_NODE_DATA="/var/packages/filebot-node/target/data" 4 | export FILEBOT_NODE_TASK="$1" 5 | 6 | 7 | # sanity check 8 | if [ ! -f "$FILEBOT_NODE_DATA/task/$FILEBOT_NODE_TASK.args" ]; then 9 | echo "$0: Task $FILEBOT_NODE_TASK does not exist" 10 | exit 1 11 | fi 12 | 13 | 14 | # import user environment 15 | . "$FILEBOT_NODE_DATA/environment.sh" 16 | 17 | 18 | # execute filebot task and record output 19 | filebot "@$FILEBOT_NODE_DATA/task/$FILEBOT_NODE_TASK.args" 2>&1 | tee -a "$FILEBOT_NODE_DATA/log/$FILEBOT_NODE_TASK.log" 20 | 21 | 22 | # get filebot exit code (and not tee exit code) 23 | STATUS="${PIPESTATUS[0]}" 24 | 25 | # treat ExitCode.NOOP as ExitCode.SUCCESS 26 | if [ $STATUS -eq 100 ]; then 27 | exit 0 28 | fi 29 | 30 | exit $STATUS 31 | -------------------------------------------------------------------------------- /package/synology/target/client/auth: -------------------------------------------------------------------------------- 1 | {"success":true,"data":{"auth":"SYNO"}} -------------------------------------------------------------------------------- /package/synology/target/client/config: -------------------------------------------------------------------------------- 1 | { 2 | ".url": { 3 | "FileBot.NodeClient": { 4 | "type": "legacy", 5 | "title": "FileBot Node", 6 | "desc": "FileBot Node allows you to execute filebot calls via Synology DSM", 7 | "icon": "images/filebot_node_{0}.png", 8 | "url": "/webman/3rdparty/filebot-node/index.html", 9 | "allUsers": true 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /package/synology/target/client/environment.cgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . /usr/syno/synoman/webman/3rdparty/filebot-node/proxy_pass.cgi 3 | -------------------------------------------------------------------------------- /package/synology/target/client/execute.cgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . /usr/syno/synoman/webman/3rdparty/filebot-node/proxy_pass.cgi 3 | -------------------------------------------------------------------------------- /package/synology/target/client/folders.cgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . /usr/syno/synoman/webman/3rdparty/filebot-node/proxy_pass.cgi 3 | -------------------------------------------------------------------------------- /package/synology/target/client/images/filebot_node_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebot/filebot-node/f1ecfa0d9a16ec0598aeb141e04cf4dd87b0f6ae/package/synology/target/client/images/filebot_node_16.png -------------------------------------------------------------------------------- /package/synology/target/client/images/filebot_node_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebot/filebot-node/f1ecfa0d9a16ec0598aeb141e04cf4dd87b0f6ae/package/synology/target/client/images/filebot_node_24.png -------------------------------------------------------------------------------- /package/synology/target/client/images/filebot_node_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebot/filebot-node/f1ecfa0d9a16ec0598aeb141e04cf4dd87b0f6ae/package/synology/target/client/images/filebot_node_256.png -------------------------------------------------------------------------------- /package/synology/target/client/images/filebot_node_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebot/filebot-node/f1ecfa0d9a16ec0598aeb141e04cf4dd87b0f6ae/package/synology/target/client/images/filebot_node_32.png -------------------------------------------------------------------------------- /package/synology/target/client/images/filebot_node_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebot/filebot-node/f1ecfa0d9a16ec0598aeb141e04cf4dd87b0f6ae/package/synology/target/client/images/filebot_node_48.png -------------------------------------------------------------------------------- /package/synology/target/client/images/filebot_node_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebot/filebot-node/f1ecfa0d9a16ec0598aeb141e04cf4dd87b0f6ae/package/synology/target/client/images/filebot_node_64.png -------------------------------------------------------------------------------- /package/synology/target/client/images/filebot_node_72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filebot/filebot-node/f1ecfa0d9a16ec0598aeb141e04cf4dd87b0f6ae/package/synology/target/client/images/filebot_node_72.png -------------------------------------------------------------------------------- /package/synology/target/client/kill.cgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . /usr/syno/synoman/webman/3rdparty/filebot-node/proxy_pass.cgi 3 | -------------------------------------------------------------------------------- /package/synology/target/client/output.cgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . /usr/syno/synoman/webman/3rdparty/filebot-node/proxy_pass.cgi 3 | -------------------------------------------------------------------------------- /package/synology/target/client/proxy_pass.cgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PROXY_PASS='http://127.0.0.1:5452' 4 | PROXY_FILE="$(basename "$SCRIPT_NAME" '.cgi')" 5 | 6 | exec -c -a 'proxy_pass.cgi' \ 7 | curl \ 8 | --include \ 9 | --silent \ 10 | --no-buffer \ 11 | --header "If-Modified-Since: $HTTP_IF_MODIFIED_SINCE" \ 12 | --header "X-Syno-Token: $HTTP_X_SYNO_TOKEN" \ 13 | --header "X-Real-IP: $REMOTE_ADDR" \ 14 | --cookie "$HTTP_COOKIE" \ 15 | "$PROXY_PASS/$PROXY_FILE?$QUERY_STRING" 16 | -------------------------------------------------------------------------------- /package/synology/target/client/schedule.cgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . /usr/syno/synoman/webman/3rdparty/filebot-node/proxy_pass.cgi 3 | -------------------------------------------------------------------------------- /package/synology/target/client/state.cgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . /usr/syno/synoman/webman/3rdparty/filebot-node/proxy_pass.cgi 3 | -------------------------------------------------------------------------------- /package/synology/target/client/task.cgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . /usr/syno/synoman/webman/3rdparty/filebot-node/proxy_pass.cgi 3 | -------------------------------------------------------------------------------- /package/synology/target/client/tasks.cgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . /usr/syno/synoman/webman/3rdparty/filebot-node/proxy_pass.cgi 3 | -------------------------------------------------------------------------------- /package/synology/target/client/test.cgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo 'Status: 200 OK' 4 | echo 'Content-Type: text/plain; charset=UTF-8' 5 | echo '' 6 | 7 | if /usr/syno/synoman/webman/authenticate.cgi; then 8 | echo '---------- printenv ----------' 9 | printenv 10 | echo '---------- id ----------' 11 | id 12 | echo '---------- node ----------' 13 | node -v 2>&1 14 | echo '---------- java ----------' 15 | java -version 2>&1 16 | echo '---------- filebot ----------' 17 | filebot -script fn:sysinfo 2>&1 18 | echo '---------- filebot-node ----------' 19 | cat /var/packages/filebot-node/target/data/filebot-node.log 20 | else 21 | echo $? 22 | fi 23 | -------------------------------------------------------------------------------- /package/synology/target/client/version.cgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . /usr/syno/synoman/webman/3rdparty/filebot-node/proxy_pass.cgi 3 | -------------------------------------------------------------------------------- /server-nodejs/README.md: -------------------------------------------------------------------------------- 1 | Run 2 | --- 3 | 4 | sh start.sh 5 | 6 | -------------------------------------------------------------------------------- /server-nodejs/app.js: -------------------------------------------------------------------------------- 1 | // PROCESS NAME 2 | process.title = 'filebot-node' 3 | 4 | // INCLUDES 5 | const http = require('http') 6 | const https = require('https') 7 | const url = require('url') 8 | const querystring = require('querystring') 9 | const child_process = require('child_process') 10 | const fs = require('fs') 11 | const path = require('path') 12 | const shellescape = require('shell-escape') 13 | const xmlParser = require('fast-xml-parser') 14 | const httpBasicAuth = require('basic-auth') 15 | 16 | // CONFIGURATION AND GLOBAL VARIABLES 17 | const DATA = process.env['FILEBOT_NODE_DATA'] 18 | const AUTH = process.env['FILEBOT_NODE_AUTH'] 19 | const CLIENT = process.env['FILEBOT_NODE_CLIENT'] 20 | const TASK_CMD = process.env['FILEBOT_TASK_CMD'] 21 | const FILEBOT_CMD = process.env['FILEBOT_CMD'] 22 | const FILEBOT_CMD_CWD = process.env['FILEBOT_CMD_CWD'] 23 | const FILEBOT_CMD_UID = parseInt(process.env['FILEBOT_CMD_UID'], 10) 24 | const FILEBOT_CMD_GID = parseInt(process.env['FILEBOT_CMD_GID'], 10) 25 | 26 | const PUBLIC_HTML = CLIENT ? '/' : '' 27 | const ROUTES = new RegExp('^/[a-z]+$') 28 | 29 | const MIME_TYPES = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.png': 'image/png', '.gif': 'image/gif', '.json': 'application/json', '.log': 'text/plain; charset=utf-8'} 30 | const SYSTEM_FILES = /^([.@#].+|bin|initrd|opt|sbin|var|dev|lib|lib32|lib64|config|proc|sys|var.defaults|etc|lost.found|root|tmp|etc.defaults|mnt|run|usr|home|homes|external|rpc|.+[.]backupbundle|NetBackup|\w+_tmp|tmp_\w+|new_\w+|CACHEDEV\w+_DATA|HD\w+_DATA|System.Volume.Information)$/ 31 | const DASHLINE = '------------------------------------------' 32 | const NEWLINE = '\n' 33 | const WRAP = '\n\n' 34 | const SIGKILL_EXIT_CODE = 137 35 | const SCHEDULED_TASK_CODE = 1000 36 | 37 | // INITIALIZERS 38 | const AUTH_CACHE = {} 39 | const ACTIVE_PROCESSES = {} 40 | const TASKS = [] 41 | 42 | // update task list via If-Last-Modified 43 | TASKS.lastModified = Date.now() 44 | 45 | const DATA_FOLDER = path.resolve(DATA) 46 | const LOG_FOLDER = path.resolve(DATA_FOLDER, 'log') 47 | const TASK_FOLDER = path.resolve(DATA_FOLDER, 'task') 48 | const TASK_INDEX = path.resolve(DATA_FOLDER, 'schedule.ids') 49 | const STATE_JSON = path.resolve(DATA_FOLDER, 'state.json') 50 | const ENVIRONMENT_SCRIPT = path.resolve(DATA_FOLDER, 'environment.sh') 51 | const FILEBOT_LOG = path.resolve(DATA_FOLDER, 'filebot.log') 52 | 53 | // create folder if necessary 54 | if (!fs.existsSync(DATA_FOLDER)) { 55 | fs.mkdirSync(DATA_FOLDER) 56 | } 57 | if (!fs.existsSync(TASK_FOLDER)) { 58 | fs.mkdirSync(TASK_FOLDER) 59 | } 60 | if (!fs.existsSync(LOG_FOLDER)) { 61 | fs.mkdirSync(LOG_FOLDER) 62 | fs.chownSync(LOG_FOLDER, FILEBOT_CMD_UID, FILEBOT_CMD_GID) // FILEBOT USER MUST BE ABLE TO WRITE LOGS 63 | } 64 | if (!fs.existsSync(FILEBOT_LOG)) { 65 | fs.writeFileSync(FILEBOT_LOG, '# Created on ' + (new Date().toString()) + NEWLINE) 66 | fs.chownSync(FILEBOT_LOG, FILEBOT_CMD_UID, FILEBOT_CMD_GID) // FILEBOT USER MUST BE ABLE TO WRITE LOGS 67 | } 68 | 69 | if (fs.existsSync(TASK_INDEX)) { 70 | fs.readFileSync(TASK_INDEX, {'encoding': 'UTF-8'}).split(/\n/).forEach(function(id) { 71 | if (id) { 72 | var stats = fs.statSync(getLogFile(id)) 73 | var mtime = new Date(stats.mtime).getTime() 74 | 75 | TASKS.push({ id: id, date: mtime, status: SCHEDULED_TASK_CODE }) 76 | } 77 | }) 78 | } 79 | 80 | 81 | /* ------------------------------------------ FileBot Command ------------------------------------------ */ 82 | 83 | 84 | function getLogFile(id) { 85 | return path.join(LOG_FOLDER, id + '.log') 86 | } 87 | 88 | function getCommand() { 89 | return FILEBOT_CMD 90 | } 91 | 92 | function getCommandArguments(options) { 93 | var args = [] 94 | if (options.fn == 'amc') { 95 | args.push('-script') 96 | args.push(options.channel == 'dev' ? 'dev:amc' : 'fn:amc') 97 | if (options.input) { 98 | args.push(options.input) 99 | } 100 | if (options.output) { 101 | args.push('--output') 102 | args.push(options.output) 103 | } 104 | if (options.action) { 105 | args.push('--action') 106 | args.push(options.action) 107 | } 108 | if (options.strict != 'no') { 109 | args.push('-non-strict') 110 | } 111 | if (options.order) { 112 | args.push('--order') 113 | args.push(options.order) 114 | } 115 | if (options.conflict) { 116 | args.push('--conflict') 117 | args.push(options.conflict) 118 | } 119 | if (options.query) { 120 | args.push('--q') 121 | args.push(options.query) 122 | } 123 | if (options.filter) { 124 | args.push('--filter') 125 | args.push(options.filter) 126 | } 127 | if (options.mapper) { 128 | args.push('--mapper') 129 | args.push(options.mapper) 130 | } 131 | if (options.lang) { 132 | args.push('--lang') 133 | args.push(options.lang) 134 | } 135 | args.push('--def') 136 | if (options.label) args.push('ut_label=' + options.label) 137 | if (options.music != 'no') args.push('music=y') 138 | if (options.unsorted != 'no') args.push('unsorted=y') 139 | if (options.excludeLink == 'on') args.push('excludeLink=y') 140 | if (options.artwork == 'on') args.push('artwork=y') 141 | if (options.subtitles) args.push('subtitles=' + options.subtitles) 142 | if (options.clean == 'on') args.push('clean=y') 143 | if (options.archives == 'skip') { 144 | args.push('skipExtract=y') 145 | } else if (options.archives == 'extract-delete') { 146 | args.push('deleteAfterExtract=y') 147 | } 148 | if (options.ignore) args.push('ignore=' + options.ignore) 149 | if (options.minLengthMS) args.push('minLengthMS=' + options.minLengthMS) 150 | if (options.minFileSize) args.push('minFileSize=' + options.minFileSize) 151 | if (options.minFileAge) args.push('minFileAge=' + options.minFileAge) 152 | if (options.exec) args.push('exec=' + options.exec) 153 | if (options.plex) args.push('plex=' + options.plex) 154 | if (options.kodi) args.push('kodi=' + options.kodi) 155 | if (options.emby) args.push('emby=' + options.emby) 156 | if (options.jellyfin) args.push('jellyfin=' + options.jellyfin) 157 | if (options.pushover) args.push('pushover=' + options.pushover) 158 | if (options.pushbullet) args.push('pushbullet=' + options.pushbullet) 159 | if (options.discord) args.push('discord=' + options.discord) 160 | if (options.report) args.push('storeReport=' + options.report) 161 | if (options.seriesFormat) args.push('seriesFormat=' + options.seriesFormat) 162 | if (options.animeFormat) args.push('animeFormat=' + options.animeFormat) 163 | if (options.movieFormat) args.push('movieFormat=' + options.movieFormat) 164 | if (options.musicFormat) args.push('musicFormat=' + options.musicFormat) 165 | if (options.movieDB) args.push('movieDB=' + options.movieDB) 166 | if (options.seriesDB) args.push('seriesDB=' + options.seriesDB) 167 | if (options.animeDB) args.push('animeDB=' + options.animeDB) 168 | if (options.unsortedFormat) args.push('unsortedFormat=' + options.unsortedFormat) 169 | if (options.excludeList) args.push('excludeList=' + options.excludeList) 170 | // --apply options 171 | var apply = ['--apply'] 172 | if (options.import == 'on') apply.push('import') 173 | if (options.metadata == 'on') apply.push('metadata') 174 | if (options.chmod == 'on') apply.push('chmod') 175 | if (options.thumbnail == 'on') apply.push('thumbnail') 176 | if (options.refresh == 'on') apply.push('refresh') 177 | if (options.apply) apply.push(options.apply) 178 | if (apply.length > 1) { 179 | args = args.concat(apply) 180 | } 181 | if (options.probe == 'no') args.push('-no-probe') 182 | if (options.index == 'no') args.push('-no-index') 183 | if (options.log) { 184 | args.push('--log') 185 | args.push(options.log) 186 | } 187 | // --def ut_* options for custom commands executed via curl 188 | var ut_options = ['--def'] 189 | if (options.ut_dir) ut_options.push('ut_dir=' + options.ut_dir) 190 | if (options.ut_file) ut_options.push('ut_file=' + options.ut_file) 191 | if (options.ut_label) ut_options.push('ut_label=' + options.ut_label) 192 | if (options.ut_title) ut_options.push('ut_title=' + options.ut_title) 193 | if (options.ut_kind) ut_options.push('ut_kind=' + options.ut_kind) 194 | if (options.ut_state) ut_options.push('ut_state=' + options.ut_state) 195 | if (ut_options.length > 1) { 196 | args = args.concat(ut_options) 197 | } 198 | } else if (options.fn == 'license' && options.license) { 199 | args.push('--license') 200 | args.push(options.license) 201 | } else if (options.fn == 'revert') { 202 | args.push('-revert') 203 | } else if (options.fn == 'sysinfo') { 204 | args.push('-script') 205 | args.push('fn:sysinfo') 206 | } else if (options.fn == 'clear') { 207 | args.push('-clear-cache') 208 | args.push('-clear-history') 209 | } else if (options.fn == 'configure') { 210 | args.push('-script') 211 | args.push('fn:configure') 212 | args.push('--def') 213 | args.push('osdbUser=' + options.osdbUser) 214 | args.push('osdbPwd=' + options.osdbPwd) 215 | } else if (options.fn == 'properties') { 216 | args.push('-script') 217 | args.push('fn:properties') 218 | args.push('--def') 219 | args.push(options.name + '=' + options.value) 220 | } else if (options.fn == 'mediainfo') { 221 | args.push('-script') 222 | args.push(options.channel == 'dev' ? 'dev:mediainfo' : 'fn:mediainfo') 223 | args.push(options.input) 224 | args.push('--mode') 225 | args.push('raw') 226 | } else { 227 | throw new Error('Illegal options: ' + JSON.stringify(options)) 228 | } 229 | 230 | // require --log-file because otherwise it will default to lock.log anyway 231 | args.push('--log-file') 232 | args.push(FILEBOT_LOG) 233 | 234 | return args 235 | } 236 | 237 | function getExitStatus(code) { 238 | var status = NEWLINE + DASHLINE + WRAP 239 | if (code == null) { 240 | status += '[Process killed]' 241 | } else if (code == 0 || code == 100) { 242 | status += '[Process completed]' 243 | } else if (code == -2 || code == 'ENOENT') { 244 | status += '[Process error]' 245 | status += WRAP + getCommand() + ': command not found' 246 | status += WRAP + '⚠️ FileBot is not installed. FileBot Node requires FileBot. Please install FileBot first and then try again.' 247 | } else { 248 | status += '[Process error]' 249 | status += WRAP + '🔺 Exit Code: ' + code 250 | // Whoopsies! --action TEST requires a valid license. 251 | if (code == 2) { 252 | status += WRAP + '💡 Please use an interactive terminal (i.e. SSH) to evaluate the filebot command-line tool.' 253 | status += WRAP + '💡 FileBot Node generates and executes filebot commands but cannot itself be used to evaluate the filebot command-line tool.' 254 | } 255 | // java: command not found 256 | if (code == 127) { 257 | status += WRAP + '💡 FileBot Node requires FileBot and Java. Please ensure that FileBot and Java are installed.' 258 | } 259 | } 260 | return status + WRAP 261 | } 262 | 263 | function spawnChildProcess(command, arguments) { 264 | // just use current time in millis as process id 265 | const id = 'R' + Date.now() 266 | const logFile = getLogFile(id) 267 | 268 | const pd = { id: id, date: Date.now(), status: null } 269 | 270 | // each log contains the original command (as JSON) in the first line 271 | fs.writeFileSync(logFile, shellescape([command].concat(arguments)) + WRAP + DASHLINE + WRAP) 272 | fs.chownSync(logFile, FILEBOT_CMD_UID, FILEBOT_CMD_GID) 273 | 274 | const out = fs.openSync(logFile, 'a') 275 | const child = child_process.spawn(command, arguments, { 276 | stdio: ['ignore', out, out], 277 | env: process.env, 278 | cwd: FILEBOT_CMD_CWD, 279 | uid: FILEBOT_CMD_UID, 280 | gid: FILEBOT_CMD_GID, 281 | // new process group leader so we can kill the entire group with kill -pid 282 | detached: true 283 | } 284 | ) 285 | 286 | child.on('error', function(error) { 287 | console.log(command, error) 288 | }) 289 | child.on('close', function(code) { 290 | // remove process object reference 291 | delete ACTIVE_PROCESSES[id] 292 | // store exit code 293 | pd.status = code != null ? code : SIGKILL_EXIT_CODE 294 | TASKS.lastModified = Date.now() 295 | 296 | // add status message 297 | fs.writeSync(out, getExitStatus(code)) 298 | fs.closeSync(out) 299 | }) 300 | 301 | ACTIVE_PROCESSES[id] = child 302 | TASKS.push(pd) 303 | TASKS.lastModified = Date.now() 304 | 305 | return pd 306 | } 307 | 308 | 309 | /* ------------------------------------------ Routes ------------------------------------------ */ 310 | 311 | 312 | function version() { 313 | const child = child_process.spawnSync(getCommand(), ['-version'], { 314 | stdio: ['ignore', 'pipe', 'pipe'], 315 | encoding: 'UTF-8', 316 | env: process.env, 317 | cwd: FILEBOT_CMD_CWD, 318 | uid: FILEBOT_CMD_UID, 319 | gid: FILEBOT_CMD_GID 320 | } 321 | ) 322 | if (child.error && child.error.code) { 323 | return getExitStatus(child.error.code) 324 | } 325 | return [child.stdout, child.stderr].join(WRAP).trim() 326 | } 327 | 328 | function state(options) { 329 | // PUT STATE 330 | if (options.store) { 331 | fs.writeFileSync(STATE_JSON, options.store) 332 | } 333 | 334 | // GET STATE 335 | else if (fs.existsSync(STATE_JSON)) { 336 | return fs.readFileSync(STATE_JSON, {'encoding': 'UTF-8'}) 337 | } 338 | 339 | return null 340 | } 341 | 342 | function environment(options) { 343 | // PUT ENV 344 | if (options.environment != null) { 345 | fs.writeFileSync(ENVIRONMENT_SCRIPT, options.environment) 346 | return { environment: null, message: "The environment has been set. Please restart the FileBot Node process to reload the environment." } 347 | } 348 | 349 | // GET ENV 350 | var environment = "" 351 | if (fs.existsSync(ENVIRONMENT_SCRIPT)) { 352 | environment = fs.readFileSync(ENVIRONMENT_SCRIPT, {'encoding': 'UTF-8'}) 353 | } 354 | return { environment: environment, message: null } 355 | } 356 | 357 | function task(request, response, options) { 358 | var id = options.id 359 | 360 | response.setHeader('Access-Control-Allow-Origin', '*') 361 | response.setHeader('Cache-Control', 'private, max-age=0, no-cache, must-revalidate') 362 | response.setHeader('Connection', 'Keep-Alive') 363 | 364 | // disable response caching to display response stream in real time 365 | response.setHeader('Content-Type', 'text/plain; charset=UTF-8') 366 | response.setHeader('X-Content-Type-Options', 'nosniff') 367 | 368 | // enable HTTP 1.1 Trailer (use curl --raw /task to see Exit-Code trailer value) 369 | response.setHeader('Transfer-Encoding', 'chunked') 370 | response.setHeader('Trailer', 'Exit-Code') 371 | 372 | // try to avoid socket timeout 373 | response.setTimeout(3 * 24 * 60 * 60 * 1000) 374 | 375 | // flush headers 376 | response.write(TASK_CMD + " " + id + WRAP + DASHLINE + WRAP) 377 | 378 | var child = child_process.spawn(TASK_CMD, [id], { 379 | stdio: ['ignore', 'pipe', 'pipe'], 380 | encoding: 'UTF-8', 381 | env: process.env, 382 | cwd: FILEBOT_CMD_CWD, 383 | uid: FILEBOT_CMD_UID, 384 | gid: FILEBOT_CMD_GID 385 | } 386 | ) 387 | 388 | child.stdout.pipe(response, {end: false}) 389 | child.stderr.pipe(response, {end: false}) 390 | 391 | child.on('close', function(code) { 392 | response.write(getExitStatus(code)) 393 | response.addTrailers({ "Exit-Code": code }) 394 | response.end() 395 | }) 396 | } 397 | 398 | function command(request, response) { 399 | response.setHeader('Access-Control-Allow-Origin', '*') 400 | response.setHeader('Cache-Control', 'private, max-age=0, no-cache, must-revalidate') 401 | response.setHeader('Connection', 'Keep-Alive') 402 | 403 | // disable response caching to display response stream in real time 404 | response.setHeader('Content-Type', 'text/plain; charset=UTF-8') 405 | response.setHeader('X-Content-Type-Options', 'nosniff') 406 | 407 | // enable HTTP 1.1 Trailer (use curl --raw /task to see Exit-Code trailer value) 408 | response.setHeader('Transfer-Encoding', 'chunked') 409 | response.setHeader('Trailer', 'Exit-Code') 410 | 411 | // try to avoid socket timeout 412 | response.setTimeout(3 * 24 * 60 * 60 * 1000) 413 | 414 | // read post body 415 | var body = '' 416 | request.on('data', function(data) { 417 | body += data; 418 | }) 419 | request.on('end', function() { 420 | try { 421 | // read argument list from HTTP POST body 422 | const args = [] 423 | 424 | if (request.headers['content-type'] == 'application/json') { 425 | // argument list as JSON array 426 | JSON.parse(body).forEach(function(argument) { args.push(argument.toString()) }) 427 | } else { 428 | // argument list as line-by-line plain text 429 | body.split(/[\r\n]+/g).forEach(function(line) { if (line.length > 0) args.push(line) }) 430 | } 431 | 432 | // require --log-file because otherwise it will default to lock.log anyway 433 | args.push('--log-file', FILEBOT_LOG) 434 | 435 | response.write(DASHLINE + WRAP + getCommand() + NEWLINE + args.join(NEWLINE) + WRAP + DASHLINE + WRAP) 436 | 437 | var child = child_process.spawn(getCommand(), args, { 438 | stdio: ['ignore', 'pipe', 'pipe'], 439 | encoding: 'UTF-8', 440 | env: process.env, 441 | cwd: FILEBOT_CMD_CWD, 442 | uid: FILEBOT_CMD_UID, 443 | gid: FILEBOT_CMD_GID 444 | } 445 | ) 446 | 447 | child.stdout.pipe(response, {end: false}) 448 | child.stderr.pipe(response, {end: false}) 449 | 450 | child.on('close', function(code) { 451 | response.write(getExitStatus(code)) 452 | response.addTrailers({ "Exit-Code": code }) 453 | response.end() 454 | }) 455 | } catch(e) { 456 | return error(response, e) 457 | } 458 | }) 459 | } 460 | 461 | function execute(options) { 462 | var pd = spawnChildProcess(getCommand(), getCommandArguments(options)) 463 | return pd 464 | } 465 | 466 | function kill(options) { 467 | var id = options.id 468 | var child = ACTIVE_PROCESSES[id] 469 | 470 | if (child) { 471 | // remove process object reference 472 | delete ACTIVE_PROCESSES[id] 473 | 474 | // if pid is less than -1, then sig is sent to every process in the process group whose ID is -pid 475 | process.kill(-child.pid) 476 | 477 | return {id: id, status: SIGKILL_EXIT_CODE} 478 | } else { 479 | throw new Error('No such process') 480 | } 481 | } 482 | 483 | function listFolders(options) { 484 | var folder = options.q 485 | var file = null 486 | 487 | folder = folder && folder[0] == '/' ? folder : FILEBOT_CMD_CWD 488 | while(!fs.existsSync(folder)) { 489 | file = path.basename(folder) 490 | folder = path.dirname(folder) 491 | } 492 | 493 | var folders = [] 494 | if (folder && fs.lstatSync(folder).isDirectory()) { 495 | fs.readdirSync(folder).forEach(function(s) { 496 | if (!SYSTEM_FILES.test(s) && (file == null || s.indexOf(file) == 0)) { 497 | var f = path.resolve(folder, s) 498 | if (fs.existsSync(f) && fs.statSync(f).isDirectory()) { 499 | folders.push({path: f}) 500 | } 501 | } 502 | } 503 | ) 504 | } 505 | return folders 506 | } 507 | 508 | function listLogs() { 509 | return fs.readdirSync(LOG_FOLDER).map(function(s) { 510 | return s.substr(0, s.lastIndexOf('.')) 511 | }) 512 | } 513 | 514 | function status() { 515 | return { 516 | pid: process.pid, 517 | node: process.version, 518 | uptime: process.uptime().toFixed(0), 519 | date: new Date().toUTCString() 520 | } 521 | } 522 | 523 | 524 | /* ------------------------------------------ Main ------------------------------------------ */ 525 | 526 | 527 | function handleRequest(request, response) { 528 | const requestParameters = url.parse(request.url) 529 | const requestPath = requestParameters.pathname 530 | 531 | // serve static resources 532 | if (!ROUTES.test(requestPath)) { 533 | return html(request, response, requestPath) 534 | } 535 | 536 | // check if service is running 537 | if ('/status' == requestPath) { 538 | return ok(response, status()) 539 | } 540 | 541 | // require user authentication for all handlers below 542 | const options = querystring.parse(requestParameters.query) 543 | const user = auth(request, response, options) 544 | 545 | if ('/auth' == requestPath) { 546 | if (user === undefined) { 547 | return unauthorized(response, true) 548 | } else { 549 | return ok(response, {'auth': AUTH, 'user': user}) 550 | } 551 | } 552 | 553 | // AUTHENTICATION REQUIRED BEYOND THIS POINT 554 | if (!user) { 555 | return unauthorized(response) 556 | } 557 | 558 | if ('/version' == requestPath) { 559 | return ok(response, version()) 560 | } 561 | 562 | if ('/tasks' == requestPath) { 563 | if (modifiedSince(request, TASKS.lastModified)) { 564 | return ok(response, TASKS, TASKS.lastModified) 565 | } else { 566 | return notModified(response) 567 | } 568 | } 569 | 570 | if ('/folders' == requestPath) { 571 | return ok(response, listFolders(options)) 572 | } 573 | 574 | if ('/output' == requestPath) { 575 | const id = options.id 576 | if (id) { 577 | return file(request, response, getLogFile(id), MIME_TYPES['.log'], false, false) 578 | } else { 579 | return file(request, response, FILEBOT_LOG, MIME_TYPES['.log'], true, true) 580 | } 581 | } 582 | 583 | if ('/execute' == requestPath) { 584 | return ok(response, execute(options)) 585 | } 586 | 587 | if ('/schedule' == requestPath) { 588 | return schedule(request, response, options) 589 | } 590 | 591 | if ('/task' == requestPath) { 592 | return task(request, response, options) 593 | } 594 | 595 | if ('/command' == requestPath) { 596 | return command(request, response) 597 | } 598 | 599 | if ('/kill' == requestPath) { 600 | return ok(response, kill(options)) 601 | } 602 | 603 | if ('/state' == requestPath) { 604 | return ok(response, state(options)) 605 | } 606 | 607 | if ('/environment' == requestPath) { 608 | return ok(response, environment(options)) 609 | } 610 | 611 | return error(response, 'BAD ROUTE') 612 | } 613 | 614 | 615 | function html(request, response, requestPath) { 616 | if (PUBLIC_HTML && requestPath.indexOf(PUBLIC_HTML) == 0) { 617 | const requestedFile = requestPath == PUBLIC_HTML ? 'index.html' : requestPath.substring(PUBLIC_HTML.length) 618 | const ext = path.extname(requestedFile) 619 | const contentType = MIME_TYPES[ext] 620 | if (contentType) { 621 | return file(request, response, path.resolve(CLIENT, requestedFile), contentType, true, false) // resolve against CLIENT folder 622 | } 623 | } 624 | return notFound(response) 625 | } 626 | 627 | 628 | /* ------------------------------------------ HTTP Response ------------------------------------------ */ 629 | 630 | 631 | function modifiedSince(request, lastModified) { 632 | var header = request.headers['if-modified-since'] 633 | if (header) { 634 | var lastModifiedInSeconds = Math.floor(lastModified / 1000) 635 | var ifModifiedSinceInSeconds = Date.parse(header) / 1000 // UTC STRING IS ONLY IN SECONDS PRECISION !!! 636 | return lastModifiedInSeconds > ifModifiedSinceInSeconds 637 | } 638 | return true // assume modified by default 639 | } 640 | 641 | function ok(response, data, lastModified) { 642 | var result = {success: true, data: data} 643 | 644 | response.statusCode = 200 645 | response.setHeader('Content-Type', 'application/json') 646 | response.setHeader('Access-Control-Allow-Origin', '*') 647 | if (lastModified > 0) { 648 | response.setHeader('Cache-Control', 'Cache-Control: private, max-age=0, no-cache, must-revalidate') 649 | response.setHeader('Last-Modified', new Date(lastModified).toUTCString()) 650 | } 651 | response.end(JSON.stringify(result)) 652 | } 653 | 654 | function file(request, response, file, contentType, cacheable, attachment) { 655 | fs.stat(file, function(err, stats) { 656 | if (err) { 657 | return notFound(response) 658 | } 659 | if (modifiedSince(request, stats.mtime.getTime())) { 660 | var readStream = fs.createReadStream(file) 661 | readStream.on('open', function() { 662 | response.statusCode = 200 663 | response.setHeader('Content-Type', contentType) 664 | response.setHeader('Content-Length', stats.size) 665 | if (attachment) response.setHeader('Content-Disposition', 'attachment; filename="' + path.basename(file) +'"') 666 | if (!cacheable) response.setHeader('Cache-Control', 'Cache-Control: private, max-age=0, no-cache, must-revalidate') 667 | response.setHeader('Last-Modified', stats.mtime.toUTCString()) 668 | response.setHeader('Access-Control-Allow-Origin', '*') 669 | readStream.pipe(response) // response.end() is called automatically 670 | }) 671 | readStream.on('error', function(err) { 672 | return error(response, err) 673 | }) 674 | } else { 675 | return notModified(response) 676 | } 677 | }) 678 | } 679 | 680 | function notModified(response) { 681 | response.statusCode = 304 682 | response.setHeader('Access-Control-Allow-Origin', '*') 683 | response.end() 684 | } 685 | 686 | function notFound(response) { 687 | response.statusCode = 404 688 | response.setHeader('Access-Control-Allow-Origin', '*') 689 | response.end('Not Found') 690 | } 691 | 692 | function unauthorized(response, authenticate) { 693 | response.statusCode = 401 694 | if (authenticate) { 695 | response.setHeader('WWW-Authenticate', 'Basic realm="filebot-node"') 696 | } 697 | response.setHeader('Access-Control-Allow-Origin', '*') 698 | response.end('Unauthorized') 699 | } 700 | 701 | function error(response, exception) { 702 | const result = {success: false, error: exception.toString()} 703 | response.statusCode = 400 704 | response.setHeader('Content-Type', 'application/json') 705 | response.setHeader('Access-Control-Allow-Origin', '*') 706 | response.end(JSON.stringify(result)) 707 | } 708 | 709 | 710 | /* ------------------------------------------ Authentication ------------------------------------------ */ 711 | 712 | 713 | function auth(request, response, options) { 714 | switch (AUTH) { 715 | case 'SYNO': 716 | return auth_syno(request, response, options) 717 | case 'QNAP': 718 | return auth_qnap(request, response) 719 | case 'BASIC': 720 | return auth_basic_env(request, response) 721 | case 'NONE': 722 | return 'NONE' 723 | default: 724 | return null 725 | } 726 | } 727 | 728 | function auth_header(request, options) { 729 | try { 730 | switch (AUTH) { 731 | case 'SYNO': 732 | if (options.SynoToken) { 733 | return { 734 | 'cookie': request.headers['cookie'].match(/\b(id=[^;]+)/)[1], 735 | 'X-Syno-Token': options.SynoToken 736 | } 737 | } else { 738 | return { 739 | 'cookie': request.headers['cookie'].match(/\b(id=[^;]+)/)[1], 740 | 'X-Syno-Token': request.headers['x-syno-token'] 741 | } 742 | } 743 | 744 | case 'QNAP': 745 | return { 746 | 'cookie': request.headers['cookie'].match(/\b(NAS_SID=[^;]+)/)[1] 747 | } 748 | } 749 | } catch(e) { 750 | // ignore invalid cookies 751 | } 752 | return null 753 | } 754 | 755 | function auth_basic_env(request, response) { 756 | const user = httpBasicAuth(request) 757 | 758 | if (user == undefined) { 759 | return undefined // REQUEST AUTH 760 | } 761 | 762 | if (user && user.name == process.env['FILEBOT_NODE_AUTH_USER'] && user.pass == process.env['FILEBOT_NODE_AUTH_PASS']) { 763 | return user.name // AUTH OK 764 | } 765 | 766 | return null // REQUEST FAIL 767 | } 768 | 769 | function auth_syno(request, response, options) { 770 | const auth = auth_header(request, options) 771 | if (!auth) { 772 | return null 773 | } 774 | 775 | const key = JSON.stringify(auth) 776 | const user = AUTH_CACHE[key] 777 | if (user) { 778 | return user 779 | } 780 | 781 | // X-Real-IP header is set by nginx server 782 | var remoteAddress = request.headers['x-real-ip'] 783 | if (!remoteAddress) { 784 | // DSM 7 does not allow nginx reverse_proxy configuration, so we may be serving requests directly 785 | remoteAddress = request.connection.remoteAddress 786 | } 787 | 788 | // authenticate.cgi requires these and some other environment variables for authentication 789 | const cmd = '/usr/syno/synoman/webman/authenticate.cgi' 790 | const env = { 791 | 'HTTP_COOKIE': auth['cookie'], 792 | 'REMOTE_ADDR': remoteAddress, 793 | 'HTTP_X_SYNO_TOKEN': auth['X-Syno-Token'] 794 | } 795 | 796 | console.log({ 'cmd': cmd, 'env': env }) 797 | 798 | // call authenticate.cgi and capture output 799 | const pd = child_process.spawnSync(cmd , [], { 800 | stdio: ['ignore', 'pipe', 'inherit'], 801 | encoding: 'UTF-8', 802 | env: env 803 | } 804 | ) 805 | 806 | if (pd.status == 0) { 807 | const value = pd.stdout.trim() 808 | AUTH_CACHE[key] = value 809 | 810 | console.log({ 'auth': auth, 'user': value }) 811 | return value 812 | } 813 | 814 | console.log({ 'status': pd.status, 'stdout': pd.stdout, 'stderr': pd.stderr }) 815 | return null 816 | } 817 | 818 | function auth_qnap(request, response) { 819 | const auth = auth_header(request) 820 | if (!auth) { 821 | return null 822 | } 823 | 824 | const key = JSON.stringify(auth) 825 | const user = AUTH_CACHE[key] 826 | if (user) { 827 | return user 828 | } 829 | 830 | // authLogin.cgi requires QUERY_STRING sid=