28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "toby",
3 | "version": "1.0.10",
4 | "description": "A simple YouTube player",
5 | "title": "Toby - A simple YouTube player",
6 | "homepage": "https://github.com/frankhale/toby",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/frankhale/toby.git"
10 | },
11 | "private": true,
12 | "author": "Frank Hale ",
13 | "license": "GPL-3.0",
14 | "main": "./build/index.html",
15 | "node-remote": "http://localhost:62374",
16 | "user-agent": "node-webkit-%nwver",
17 | "window": {
18 | "icon": "./public/images/toby.png",
19 | "toolbar": false,
20 | "width": 640,
21 | "height": 369,
22 | "min_width": 640,
23 | "min_height": 369,
24 | "show": false
25 | },
26 | "devDependencies": {
27 | "@types/body-parser": "^1.17.1",
28 | "@types/cookie-parser": "^1.4.2",
29 | "@types/debug": "^4.1.5",
30 | "@types/express": "^4.17.2",
31 | "@types/jquery": "^3.3.31",
32 | "@types/keymaster": "^1.6.28",
33 | "@types/lodash": "^4.14.148",
34 | "@types/morgan": "^1.7.37",
35 | "@types/node": "^12.12.8",
36 | "@types/nw.js": "^0.13.8",
37 | "@types/react": "^16.9.11",
38 | "@types/react-dom": "^16.9.4",
39 | "@types/request": "^2.48.3",
40 | "@types/serve-favicon": "^2.2.31",
41 | "@types/socket.io": "^2.1.4",
42 | "@types/socket.io-client": "^1.4.32",
43 | "@types/sqlite3": "^3.1.5",
44 | "@types/youtube": "^0.0.38",
45 | "grunt": "^1.0.4",
46 | "grunt-contrib-copy": "^1.0.0",
47 | "grunt-ts": "^6.0.0-beta.22",
48 | "grunt-tslint": "^5.0.2",
49 | "source-map-loader": "^0.2.4",
50 | "ts-loader": "^6.2.1",
51 | "tslint": "^5.20.1",
52 | "typescript": "^3.7.2",
53 | "uglifyjs-webpack-plugin": "^2.2.0",
54 | "webpack": "^4.41.2"
55 | },
56 | "dependencies": {
57 | "body-parser": "^1.19.0",
58 | "cookie-parser": "^1.4.4",
59 | "debug": "^4.1.1",
60 | "express": "^4.17.1",
61 | "hbs": "^4.0.6",
62 | "jquery": "^3.5.0",
63 | "lodash": "^4.17.15",
64 | "moment": "^2.24.0",
65 | "morgan": "^1.9.1",
66 | "node": "^12.13.0",
67 | "react": "^16.12.0",
68 | "react-dom": "^16.12.0",
69 | "request": "^2.88.0",
70 | "serve-favicon": "^2.5.0",
71 | "socket.io": "^2.3.0",
72 | "split": "^1.0.1",
73 | "sqlite3": "^4.1.0",
74 | "title-case": "^2.1.1",
75 | "youtube-search": "^1.1.4"
76 | },
77 | "webview": {
78 | "partitions": [
79 | {
80 | "name": "trusted",
81 | "accessible_resources": [
82 | ""
83 | ]
84 | }
85 | ]
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/public/images/toby.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frankhale/toby/521d847a44fb331c342d0565841803243ee6fa44/public/images/toby.ico
--------------------------------------------------------------------------------
/public/images/toby.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frankhale/toby/521d847a44fb331c342d0565841803243ee6fa44/public/images/toby.png
--------------------------------------------------------------------------------
/public/stylesheets/app.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 0;
3 | margin: 0;
4 | background-color: #000;
5 | color: #fff;
6 | font-size: 18pt;
7 | overflow: hidden;
8 | }
9 |
10 | #ui {
11 | position: absolute;
12 | width: 100%;
13 | height: 100%;
14 | overflow: hidden;
15 | }
16 |
17 | #ui:hover {
18 | overflow-y: scroll;
19 | }
20 |
21 | #player {
22 | position: absolute;
23 | width: 100%;
24 | height: 100%;
25 | display: none;
26 | }
27 |
28 | * {
29 | font-family: "Share Tech Mono", monospace;
30 | }
31 |
32 | pre {
33 | outline: none !important;
34 | border-left: 4px solid black;
35 | background-color: #eee;
36 | white-space: pre-wrap;
37 | margin-left: 40px !important;
38 | margin-right: 40px !important;
39 | }
40 |
41 | blockquote {
42 | border-left: 4px solid black;
43 | background-color: #eee;
44 | padding: 10px;
45 | }
46 |
47 | /* Command Input */
48 |
49 | .command-container {
50 | padding-left: 10px;
51 | font-size: 18pt;
52 | }
53 |
54 | .command-input {
55 | caret-color: white;
56 | border: none;
57 | outline: none;
58 | padding-left: 2px;
59 | padding-top: 5px;
60 | margin-bottom: 20px;
61 | font-size: 18pt;
62 | background-color: #000;
63 | color: #fff;
64 | }
65 |
66 | /* Content Panel */
67 |
68 | .content-panel {
69 | padding-left: 10px;
70 | padding-bottom: 25px;
71 | font-size: 18px;
72 | }
73 |
74 | /* misc */
75 |
76 | .thumbnailIMGWidth {
77 | width: 50px;
78 | }
79 |
80 | .buttonContainerWidth {
81 | width: 1px;
82 | white-space: nowrap;
83 | }
84 |
85 | .textAlignMiddle {
86 | vertical-align: middle;
87 | }
88 |
89 | table {
90 | border-collapse: collapse;
91 | width: 100%;
92 | }
93 |
94 | tr:hover td {
95 | background-color: #003e80;
96 | color: #fff;
97 | text-shadow: 0 0 6px #001130;
98 | font-weight: bold;
99 | cursor: pointer;
100 | }
101 |
102 | tr:hover td.border-left {
103 | border-radius: 8px 0 0 8px;
104 | }
105 |
106 | tr:hover td.border-right {
107 | border-radius: 0 8px 8px 0;
108 | }
109 |
110 | .alignDiv {
111 | display: inline-block;
112 | vertical-align: middle;
113 | }
114 |
115 | .videoTitle {
116 | cursor: default;
117 | }
118 |
119 | .videoThumbnail {
120 | margin-top: 4px;
121 | padding: 6px;
122 | width: 90px;
123 | }
124 |
125 | .videoThumbnailSlim {
126 | margin-left: 5px;
127 | margin-top: 4px;
128 | padding: 6px;
129 | width: 80px;
130 | border: 4px solid black;
131 | }
132 |
133 | .videoThumbnailSlim:hover {
134 | border: 4px solid #003e80;
135 | }
136 |
137 | .videoAddedNotification {
138 | position: fixed;
139 | border: 2px solid #001130;
140 | background-color: #003e80;
141 | color: #fff;
142 | padding: 10px;
143 | top: 5px;
144 | right: 5px;
145 | font-size: 11pt;
146 | z-index: 10000;
147 | }
148 |
149 | .manageButton {
150 | color: #fff;
151 | vertical-align: middle;
152 | text-decoration: none;
153 | margin-right: 2px;
154 | }
155 |
156 | .manageButton:hover {
157 | color: #999;
158 | }
159 |
160 | .grayscale {
161 | -webkit-filter: grayscale(1);
162 | }
163 |
164 | .saturate {
165 | -webkit-filter: saturate(2.5);
166 | }
167 |
168 | .sepia {
169 | -webkit-filter: sepia(1);
170 | }
171 |
172 | ::-webkit-scrollbar {
173 | height: 8px;
174 | width: 8px;
175 | background: #000;
176 | }
177 |
178 | ::-webkit-scrollbar-thumb {
179 | background: #999;
180 | -webkit-border-radius: 1ex;
181 | -webkit-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.75);
182 | }
183 |
184 | ::-webkit-scrollbar-corner {
185 | background: #fff;
186 | }
187 |
188 | /* Select style borrowed from: http://codepen.io/AmrSubZero/pen/dxpri */
189 |
190 | select {
191 | background-color: #357;
192 | background-repeat: no-repeat;
193 | background-position: right 10px top 10px;
194 | background-size: 11px 11px;
195 | padding: 8px;
196 | width: auto;
197 | font-family: arial, tahoma;
198 | font-size: 10px;
199 | font-weight: bold;
200 | color: #fff;
201 | text-align: center;
202 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
203 | border-radius: 3px;
204 | /*-webkit-appearance: none;*/
205 | border: 0;
206 | outline: 0;
207 | -webkit-transition: 0.3s ease all;
208 | transition: 0.3s ease all;
209 | }
210 |
211 | select:focus,
212 | select:active {
213 | border: 0;
214 | outline: 0;
215 | }
216 |
217 | .groupDropDown:hover {
218 | background-color: #123;
219 | }
220 |
221 | .groupDropDownDisabled {
222 | margin-right: 5px;
223 | background-color: #333;
224 | width: 110px;
225 | }
226 |
227 | .groupDropDown {
228 | margin-right: 5px;
229 | width: 110px;
230 | }
231 |
232 | #version {
233 | position: fixed;
234 | bottom: 25px;
235 | right: 25px;
236 | font-weight: bold;
237 | color: #444;
238 | opacity: 0.5;
239 | }
--------------------------------------------------------------------------------
/public/stylesheets/main.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | background-color: #000;
4 | color: #fff;
5 | padding: 0;
6 | margin: 0;
7 | overflow: hidden;
8 | }
9 |
10 | * {
11 | font-family: "Share Tech Mono", monospace;
12 | }
13 |
14 | #content {
15 | position: absolute;
16 | width: 100%;
17 | height: 100%;
18 | overflow: auto;
19 | visibility: hidden;
20 | padding: 10px;
21 | }
22 |
23 | #loading {
24 | position: absolute;
25 | width: 100%;
26 | top: 45%;
27 | text-align: center;
28 | }
29 |
30 | #webview {
31 | position: absolute;
32 | top: 0;
33 | left: 0;
34 | width: 100%;
35 | height: 100%;
36 | z-index: 1000;
37 | display: inline-flex !important;
38 | }
39 |
40 | ::-webkit-scrollbar {
41 | height: 8px;
42 | width: 8px;
43 | background: #000;
44 | }
45 | ::-webkit-scrollbar-thumb {
46 | background: #999;
47 | -webkit-border-radius: 1ex;
48 | -webkit-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.75);
49 | }
50 | ::-webkit-scrollbar-corner {
51 | background: #fff;
52 | }
53 |
--------------------------------------------------------------------------------
/screenshots/toby-main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frankhale/toby/521d847a44fb331c342d0565841803243ee6fa44/screenshots/toby-main.png
--------------------------------------------------------------------------------
/screenshots/toby-manage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frankhale/toby/521d847a44fb331c342d0565841803243ee6fa44/screenshots/toby-manage.png
--------------------------------------------------------------------------------
/screenshots/toby-recently-played.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frankhale/toby/521d847a44fb331c342d0565841803243ee6fa44/screenshots/toby-recently-played.png
--------------------------------------------------------------------------------
/screenshots/toby-server-log.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frankhale/toby/521d847a44fb331c342d0565841803243ee6fa44/screenshots/toby-server-log.png
--------------------------------------------------------------------------------
/screenshots/toby-video-list-slim-grid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frankhale/toby/521d847a44fb331c342d0565841803243ee6fa44/screenshots/toby-video-list-slim-grid.png
--------------------------------------------------------------------------------
/screenshots/toby-video-list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frankhale/toby/521d847a44fb331c342d0565841803243ee6fa44/screenshots/toby-video-list.png
--------------------------------------------------------------------------------
/screenshots/toby-video-playback.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frankhale/toby/521d847a44fb331c342d0565841803243ee6fa44/screenshots/toby-video-playback.png
--------------------------------------------------------------------------------
/src/api.ts:
--------------------------------------------------------------------------------
1 | // api.js - Express API for Toby
2 | // Author(s): Frank Hale
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU General Public License
15 | // along with this program. If not, see .
16 |
17 | import * as _ from "lodash";
18 | import * as express from "express";
19 | import * as fs from "fs";
20 | import * as http from "http";
21 |
22 | import * as youtubeSearch from "youtube-search";
23 |
24 | import { IVideoGroup, IVideoEntry } from "./infrastructure";
25 | import { SearchCache } from "./searchCache";
26 |
27 | import AppConfig from "./config";
28 | import DB from "./db";
29 | import DefaultData from "./data";
30 |
31 | interface APIRoute {
32 | path: string;
33 | route: (req: express.Request, res: express.Response) => void;
34 | }
35 |
36 | export default class API {
37 | private db: DB;
38 | private server: http.Server;
39 | private routes: APIRoute[];
40 | private cache: SearchCache;
41 | public router: express.Router;
42 |
43 | constructor(db: DB, server: http.Server) {
44 | this.db = db;
45 | this.router = express.Router();
46 | this.server = server;
47 | this.cache = new SearchCache();
48 |
49 | this.routes = [
50 | { path: "GET /videos", route: this.getVideos },
51 | { path: "GET /videos/groups", route: this.getVideosGroups },
52 | { path: "GET /videos/archive", route: this.getVideosArchive },
53 | { path: "POST /app/close", route: this.postAppClose },
54 | {
55 | path: "POST /videos/youtube/search",
56 | route: this.postVideosYouTubeSearch
57 | },
58 | { path: "POST /videos/search", route: this.postVideosSearch },
59 | { path: "POST /videos/add", route: this.postVideosAdd },
60 | { path: "POST /videos/delete", route: this.postVideosDelete },
61 | { path: "POST /videos/update", route: this.postVideosUpdate },
62 | {
63 | path: "POST /videos/recently-played/add",
64 | route: this.postVideosRecentlyPlayedAdd
65 | },
66 | {
67 | path: "POST /videos/recently-played/search",
68 | route: this.postVideosRecentlyPlayedSearch
69 | },
70 | {
71 | path: "POST /videos/recently-played/last30",
72 | route: this.postVideosRecentlyPlayedLas30
73 | }
74 | ];
75 |
76 | this.db.importIntoDB(DefaultData.getData());
77 |
78 | this.initializeRoutes();
79 | }
80 | private initializeRoutes(): void {
81 | _.forEach(this.routes, r => {
82 | let routePath = r.path.split(" ");
83 | this.router[routePath[0].toLowerCase()](routePath[1], r.route.bind(this));
84 | });
85 | }
86 | private createDataFileString(data: IVideoGroup[]): string {
87 | return JSON.stringify(data, null, 2);
88 | }
89 | private writeDataFile(dataFilePath: string, dataString: string): void {
90 | try {
91 | fs.writeFileSync(dataFilePath, dataString, "utf8");
92 | } catch (e) {
93 | console.log(`Error writing data file: ${e}`);
94 | }
95 | }
96 | private getVideos(_req: express.Request, res: express.Response): void {
97 | this.db.getAllVideosFromDB(data => {
98 | res.json(data);
99 | });
100 | }
101 | private getVideosGroups(_req: express.Request, res: express.Response): void {
102 | this.db.getAllGroupsFromDB(data => {
103 | data = _.map(data, d => {
104 | return d.group;
105 | });
106 | res.json(data);
107 | });
108 | }
109 | private getVideosArchive(_req: express.Request, res: express.Response): void {
110 | this.db.getAllGroupsFromDB(groups => {
111 | this.db.getAllVideosOrderedByGroupDB(data => {
112 | let results: IVideoGroup[] = [];
113 |
114 | _.forEach(groups, g => {
115 | let entries = _.sortBy(_.filter(data, { group: g.group }), ["title"]);
116 |
117 | entries = _.map(entries, e => {
118 | return {
119 | title: e.title.replace(/[^\x00-\x7F]/g, ""),
120 | ytid: e.ytid
121 | };
122 | });
123 |
124 | results.push({
125 | group: g.group,
126 | entries: entries
127 | });
128 | });
129 |
130 | let dataFileString = this.createDataFileString(results);
131 | this.writeDataFile(AppConfig.dataFilePath, dataFileString);
132 |
133 | res.send(dataFileString);
134 | });
135 | });
136 | }
137 | private postAppClose(_req: Express.Request, _res: Express.Response): void {
138 | this.db.close();
139 | this.server.close();
140 | process.exit(0);
141 | }
142 | private postVideosYouTubeSearch(
143 | req: express.Request,
144 | res: express.Response
145 | ): void {
146 | let searchTerm = req.body.searchTerm;
147 |
148 | if (searchTerm.indexOf("/yt") > -1) {
149 | searchTerm = searchTerm.replace("/yt", "");
150 | }
151 |
152 | youtubeSearch(searchTerm, AppConfig.youtubeSearchOpts, (err, results) => {
153 | if (err) return console.log(err);
154 |
155 | const ytids = _.map(results, r => {
156 | return r.id;
157 | });
158 |
159 | this.db.getAllVideosWhereYTIDInList(ytids, ytids_found => {
160 | let finalResults: IVideoEntry[] = [];
161 |
162 | _.forEach(results, r => {
163 | let found = _.find(ytids_found, f => {
164 | return f.ytid === r.id;
165 | });
166 |
167 | finalResults.push({
168 | title: r.title,
169 | ytid: r.id,
170 | group: found ? found.group : "",
171 | isArchived: found ? true : false
172 | });
173 | });
174 |
175 | this.cache.addItem(searchTerm, finalResults);
176 | res.json(finalResults);
177 | });
178 | });
179 | }
180 | private postVideosSearch(req: express.Request, res: express.Response): void {
181 | let searchTerm = req.body.searchTerm;
182 |
183 | console.log(`searching for ${searchTerm} locally`);
184 |
185 | if (searchTerm.startsWith("/yt")) {
186 | this.postVideosYouTubeSearch(req, res);
187 | } else if (searchTerm.startsWith("/group") || searchTerm.startsWith("/g")) {
188 | searchTerm = _.slice(searchTerm.split(" "), 1).join(" ");
189 |
190 | if (searchTerm === "all") {
191 | this.db.getAllVideosFromDB(data => {
192 | res.json(data);
193 | });
194 | } else {
195 | this.db.getAllVideosForGroupFromDB(searchTerm, data => {
196 | res.json(data);
197 | });
198 | }
199 | } else {
200 | this.db.getVideosWhereTitleLikeFromDB(searchTerm, data => {
201 | res.json(data);
202 | });
203 | }
204 | }
205 | private postVideosAdd(req: express.Request, res: express.Response): void {
206 | let title = req.body.title,
207 | ytid = req.body.ytid,
208 | group = req.body.group;
209 |
210 | console.log(title, ytid, group);
211 |
212 | if (!(_.isEmpty(title) || _.isEmpty(ytid) || _.isEmpty(group))) {
213 | this.db.addVideoToDB(title, ytid, group);
214 | res.json({ success: true });
215 | } else {
216 | res.json({ success: false });
217 | }
218 | }
219 | private postVideosDelete(req: express.Request, res: express.Response): void {
220 | let ytid = req.body.ytid;
221 |
222 | if (ytid !== undefined && ytid.length > 0) {
223 | this.db.deleteVideoFromDB(ytid);
224 |
225 | res.json({ success: true });
226 | } else {
227 | res.json({ success: true });
228 | }
229 | }
230 | private postVideosUpdate(req: express.Request, res: express.Response): void {
231 | let title = req.body.title,
232 | ytid = req.body.ytid,
233 | group = req.body.group;
234 |
235 | if (
236 | title !== undefined &&
237 | title.length > 0 &&
238 | ytid !== undefined &&
239 | ytid.length > 0 &&
240 | group !== undefined &&
241 | group.length > 0
242 | ) {
243 | this.db.updateVideoFromDB(title, ytid, group);
244 |
245 | res.json({ success: true });
246 | } else {
247 | res.json({ success: false });
248 | }
249 | }
250 | private postVideosRecentlyPlayedAdd(
251 | req: express.Request,
252 | res: express.Response
253 | ): void {
254 | let title = req.body.title,
255 | ytid = req.body.ytid;
256 |
257 | if (
258 | title !== undefined &&
259 | title.length > 0 &&
260 | ytid !== undefined &&
261 | ytid.length > 0
262 | ) {
263 | // Recently Played is the last 30 (by default) videos played
264 |
265 | // get all of the recently played videos
266 | this.db.getAllVideosForGroupFromDB("Recently Played", data => {
267 | // If the video we are trying to add is already in the Recently Played
268 | // group then we need to exit gracefully...
269 |
270 | if (_.find(data, { ytid: ytid }) !== undefined) {
271 | let message = `${ytid} is already in the Recently Played group...`;
272 | console.log(message);
273 | res.json({
274 | success: false,
275 | message: message
276 | });
277 | } else {
278 | this.db.addVideoToDB(title, ytid, "Recently Played");
279 |
280 | res.json({ success: true });
281 | }
282 | });
283 | } else {
284 | res.json({
285 | success: false,
286 | message: "title is required but was empty or undefined"
287 | });
288 | }
289 | }
290 | private postVideosRecentlyPlayedSearch(
291 | req: express.Request,
292 | res: express.Response
293 | ): void {
294 | let searchTerm = req.body.searchTerm;
295 |
296 | if (searchTerm !== undefined && searchTerm.length > 0) {
297 | this.db.getVideosFromGroupWhereTitleLikeFromDB(
298 | searchTerm,
299 | "Recently Played",
300 | data => {
301 | res.json(data);
302 | }
303 | );
304 | } else {
305 | res.json([]);
306 | }
307 | }
308 | private postVideosRecentlyPlayedLas30(
309 | req: express.Request,
310 | res: express.Response
311 | ): void {
312 | let trim = false;
313 |
314 | if (req.body.trim !== undefined) {
315 | trim = req.body.trim;
316 | }
317 |
318 | // This is going to trim the recently played rows down to the max number
319 | // which defaults to 30
320 | this.db.getAllVideosForGroupFromDB("Recently Played", data => {
321 | // take top 30
322 | let top30RecentlyPlayed = _.takeRight(
323 | _.uniqBy(data, "ytid"),
324 | AppConfig.maxRecentlyPlayedVideos
325 | );
326 |
327 | // console.log(`before: ${data.length}`);
328 | // console.log(`after: ${top30RecentlyPlayed.length}`);
329 |
330 | if (req.body.trim) {
331 | console.log("Trimming the Recently Played group");
332 | // delete all recently played from db
333 | this.db.deleteRecentlyPlayedVideosFromDB();
334 | // add trimmed recently played back to DB
335 | _.forEach(top30RecentlyPlayed, rp => {
336 | this.db.addVideoToDB(rp.title, rp.ytid, "Recently Played");
337 | });
338 | }
339 |
340 | res.json(top30RecentlyPlayed);
341 | });
342 | }
343 | }
344 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | // config.js - App configuration information
2 | // Author(s): Frank Hale
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU General Public License
15 | // along with this program. If not, see .
16 |
17 | import * as path from "path";
18 | import * as youtubeSearch from "youtube-search";
19 |
20 | export default class AppConfig {
21 | static serverPort = "62374";
22 | static socketIOPort = "62375";
23 | static serverURL = `http://localhost:${AppConfig.serverPort}`;
24 | static maxSearchResults = 30;
25 | static maxRecentlyPlayedVideos = 30;
26 | static youtubeSearchOpts: youtubeSearch.YouTubeSearchOptions = {
27 | maxResults: AppConfig.maxSearchResults,
28 | key: process.env.YOUTUBE_API_KEY,
29 | type: "video"
30 | };
31 | static dataPath = `${__dirname}${path.sep}..${path.sep}data`;
32 | static dataFilePath = `${AppConfig.dataPath}${path.sep}data.json`;
33 | }
34 |
--------------------------------------------------------------------------------
/src/data.ts:
--------------------------------------------------------------------------------
1 | // data.ts - Default data to populate database with
2 | // Copyright (C) 2016-2017 Frank Hale
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU General Public License
15 | // along with this program. If not, see .
16 |
17 | import * as fs from "fs";
18 | import AppConfig from "./config";
19 | import { IVideoGroup } from "./infrastructure";
20 |
21 | export default class DefaultData {
22 | static getData(): IVideoGroup[] {
23 | if (fs.existsSync(AppConfig.dataFilePath)) {
24 | // read in the data from the data.json this file is also the same
25 | // one that is created when using the archive function
26 | return JSON.parse(fs.readFileSync(AppConfig.dataFilePath).toString());
27 | } else {
28 | return [];
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/db.ts:
--------------------------------------------------------------------------------
1 | // db.ts - Database API for Toby
2 | // Author(s): Frank Hale
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU General Public License
15 | // along with this program. If not, see .
16 |
17 | import * as sqlite3 from "sqlite3";
18 | import * as path from "path";
19 | import * as _ from "lodash";
20 |
21 | import AppConfig from "./config";
22 | import { IVideoGroup } from "./infrastructure";
23 |
24 | export default class DB {
25 | private db: sqlite3.Database;
26 |
27 | constructor() {
28 | sqlite3.verbose();
29 | this.db = new sqlite3.Database(`${AppConfig.dataPath}${path.sep}videoDB`);
30 | }
31 | importIntoDB(videoData: IVideoGroup[]): void {
32 | this.db.serialize(() => {
33 | this.db.run(
34 | "CREATE TABLE IF NOT EXISTS videos (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, ytid TEXT, [group] TEXT)"
35 | );
36 |
37 | this.db.each("SELECT COUNT(*) as count FROM videos", (err, row) => {
38 | _.forEach(videoData, (g: IVideoGroup) => {
39 | _.forEach(g.entries, e => {
40 | this.getVideoFromDB(e.ytid, row => {
41 | if (row === undefined && g.group !== "Recently Played") {
42 | console.log(`importing ${e.title} | ${g.group}`);
43 | this.db.run(
44 | "INSERT into videos(title,ytid,[group]) VALUES (?,?,?)",
45 | [e.title, e.ytid, g.group]
46 | );
47 | }
48 | });
49 | });
50 | });
51 | });
52 | });
53 | }
54 | getAllYTIDsFromDB(finished: (rows: any[]) => void): void {
55 | this.db.all(
56 | "SELECT title, ytid, [group] FROM videos WHERE [group] IS NOT 'Recently Played' COLLATE NOCASE",
57 | (_err, rows) => {
58 | if (_.isFunction(finished)) {
59 | // let ytids: string[] = [];
60 |
61 | // _.forEach(rows, d => {
62 | // ytids.push(d.ytid);
63 | // });
64 |
65 | let ytids: string[] = _.map(rows, d => {
66 | return d.ytid;
67 | });
68 |
69 | finished(ytids);
70 | }
71 | }
72 | );
73 | }
74 | getAllVideosFromDB(finished: (rows: any[]) => void): void {
75 | this.db.all(
76 | "SELECT title, ytid, [group] FROM videos WHERE [group] IS NOT 'Recently Played' COLLATE NOCASE",
77 | (_err, rows) => {
78 | if (_.isFunction(finished)) {
79 | let _rows = _.forEach(rows, d => {
80 | d.isArchived = true;
81 | });
82 |
83 | finished(_rows);
84 | }
85 | }
86 | );
87 | }
88 | getAllVideosOrderedByGroupDB(finished: (rows: any[]) => void): void {
89 | this.db.all(
90 | "SELECT title, ytid, [group] FROM videos ORDER BY [group]",
91 | (_err, rows) => {
92 | if (_.isFunction(finished)) {
93 | finished(rows);
94 | }
95 | }
96 | );
97 | }
98 | getVideoFromDB(ytid: string, finished: (row: any) => void): void {
99 | this.db.get(
100 | "SELECT title, ytid, [group] FROM videos WHERE ytid = ? AND [group] IS NOT 'Recently Played'",
101 | [ytid],
102 | (_err, row) => {
103 | if (_.isFunction(finished)) {
104 | finished(row);
105 | }
106 | }
107 | );
108 | }
109 | getAllGroupsFromDB(finished: (rows: any[]) => void): void {
110 | this.db.all("SELECT DISTINCT [group] FROM videos", (err, rows) => {
111 | if (_.isFunction(finished)) {
112 | finished(rows);
113 | }
114 | });
115 | }
116 | getAllVideosWhereYTIDInList(
117 | ytids: string[],
118 | finished: (rows: any[]) => void
119 | ): void {
120 | let ytids_in_string = _.map(ytids, r => {
121 | return `'${r}'`;
122 | }).join(",");
123 |
124 | this.db.all(
125 | `SELECT title, ytid, [group] FROM videos WHERE [group] IS NOT 'Recently Played' AND ytid IN (${ytids_in_string})`,
126 | (_err, ytids_found) => {
127 | finished(ytids_found);
128 | }
129 | );
130 | }
131 | getAllVideosForGroupFromDB(
132 | group: string,
133 | finished: (rows: any[]) => void
134 | ): void {
135 | // this.db.all("SELECT title, ytid, [group] FROM videos WHERE [group] = ? COLLATE NOCASE", [group], (err, rows) => {
136 | // if (finished !== undefined) {
137 | // let _rows = _.forEach(rows, (d) => {
138 | // d.isArchived = true;
139 | // });
140 |
141 | // finished(_rows);
142 | // }
143 | // });
144 |
145 | this.db.all(
146 | "SELECT title, ytid, [group] FROM videos WHERE [group] = ? COLLATE NOCASE",
147 | [group],
148 | (err, rows) => {
149 | if (_.isFunction(finished)) {
150 | let ytids = _.map(rows, r => {
151 | return r.ytid;
152 | }),
153 | ytids_in_string = _.map(ytids, r => {
154 | return `'${r}'`;
155 | }).join(",");
156 |
157 | this.db.all(
158 | `SELECT ytid FROM videos WHERE [group] IS NOT 'Recently Played' AND ytid IN (${ytids_in_string})`,
159 | (_err, ytids_found) => {
160 | let _ytids = _.map(ytids_found, r => {
161 | return r.ytid;
162 | }),
163 | _rows = _.forEach(rows, d => {
164 | d.isArchived =
165 | _.indexOf(_ytids, d.ytid) !== -1 ? true : false;
166 | });
167 |
168 | finished(_rows);
169 | }
170 | );
171 | }
172 | }
173 | );
174 | }
175 | getVideosWhereTitleLikeFromDB(
176 | searchTerm: string,
177 | finished: (rows: any[]) => void
178 | ): void {
179 | searchTerm = `%${searchTerm.trim()}%`;
180 |
181 | this.db.all(
182 | "SELECT title, ytid, [group] FROM videos WHERE title LIKE ? AND [group] IS NOT 'Recently Played' COLLATE NOCASE",
183 | [searchTerm],
184 | (_err, rows) => {
185 | if (_.isFunction(finished)) {
186 | let _rows = _.forEach(rows, d => {
187 | d.isArchived = true;
188 | });
189 |
190 | finished(_rows);
191 | }
192 | }
193 | );
194 | }
195 | getVideosFromGroupWhereTitleLikeFromDB(
196 | searchTerm: string,
197 | group: string,
198 | finished: (rows: any[]) => void
199 | ): void {
200 | searchTerm = `%${searchTerm.trim()}%`;
201 |
202 | this.db.all(
203 | "SELECT title, ytid, [group] FROM videos WHERE title LIKE ? AND [group] = ? COLLATE NOCASE",
204 | [searchTerm, group],
205 | (err, rows) => {
206 | if (_.isFunction(finished)) {
207 | let _rows = _.forEach(rows, d => {
208 | d.isArchived = true;
209 | });
210 |
211 | finished(_rows);
212 | }
213 | }
214 | );
215 | }
216 | addVideoToDB(title: string, ytid: string, group: string): void {
217 | if (!(_.isEmpty(title) || _.isEmpty(ytid) || _.isEmpty(group))) {
218 | this.db.get(
219 | "SELECT ytid FROM videos WHERE ytid = ? AND [group] = ? COLLATE NOCASE",
220 | [ytid, group],
221 | (_err, rows) => {
222 | if (_.isEmpty(rows)) {
223 | console.log(`inserting ${title} into ${group}`);
224 | this.db.run(
225 | "INSERT into videos(title,ytid,[group]) VALUES (?,?,?)",
226 | [title, ytid, group]
227 | );
228 | }
229 | }
230 | );
231 | }
232 | }
233 | deleteVideoFromDB(ytid: string): void {
234 | this.db.get(
235 | "SELECT ytid FROM videos WHERE ytid = ?",
236 | [ytid],
237 | (_err, rows) => {
238 | if (!_.isEmpty(rows)) {
239 | this.db.run("DELETE FROM videos WHERE ytid = ?", [ytid]);
240 | }
241 | }
242 | );
243 | }
244 | updateVideoFromDB(title: string, ytid: string, group: string): void {
245 | if (!_.isEmpty(title) && !_.isEmpty(group)) {
246 | this.db.get(
247 | "SELECT ytid FROM videos WHERE ytid = ?",
248 | [ytid],
249 | (_err, rows) => {
250 | if (_.isEmpty(rows)) {
251 | this.db.run(
252 | "UPDATE videos SET title = ?, group = ? WHERE ytid = ?",
253 | [title, group, ytid]
254 | );
255 | }
256 | }
257 | );
258 | }
259 | }
260 | deleteRecentlyPlayedVideosFromDB(): void {
261 | this.db.run("DELETE FROM videos WHERE [group] = 'Recently Played'");
262 | }
263 | close(): void {
264 | this.db.close();
265 | }
266 | }
267 |
--------------------------------------------------------------------------------
/src/electron.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | // electron.ts - Common interfaces used by Toby's server.
4 | // Author(s): Frank Hale
5 | //
6 | // This program is free software: you can redistribute it and/or modify
7 | // it under the terms of the GNU General Public License as published by
8 | // the Free Software Foundation, either version 3 of the License, or
9 | // (at your option) any later version.
10 | //
11 | // This program is distributed in the hope that it will be useful,
12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | // GNU General Public License for more details.
15 | //
16 | // You should have received a copy of the GNU General Public License
17 | // along with this program. If not, see .
18 |
19 | // NOTE:
20 | //
21 | // We are including the official Electron type definition here because we don't
22 | // explicitly install the Electron package since Toby can run on whatever the
23 | // user wants to use eg. NW.js, Electron or just the web. Additionally
24 | // @types/electron was removed from package.json as it appears to no longer be
25 | // updated and installing it will show an warning when compiling via Grunt.
26 |
27 | import * as path from "path";
28 | import { app, BrowserWindow } from "electron";
29 |
30 | let mainWindow: Electron.BrowserWindow;
31 |
32 | // found an issue with recent versions of electron in that focus would run crazy
33 | // in certain situations.
34 | //
35 | // issue: https://github.com/electron/electron/issues/7655
36 | //
37 | // this command line switch seems to make the problem go away
38 | app.commandLine.appendSwitch("enable-use-zoom-for-dsf", "false");
39 |
40 | function createWindow(): void {
41 | mainWindow = new BrowserWindow({
42 | fullscreenable: true,
43 | autoHideMenuBar: true,
44 | useContentSize: true,
45 | icon: `${__dirname}${path.sep}..${path.sep}public${path.sep}images${path.sep}toby.png`,
46 | backgroundColor: "#000",
47 | width: 640,
48 | height: 400,
49 | minWidth: 640,
50 | minHeight: 400,
51 | show: false,
52 | webPreferences: {
53 | nodeIntegration: true,
54 | webviewTag: true
55 | }
56 | });
57 | mainWindow.setMenu(null);
58 | mainWindow.loadURL(`file://${__dirname}/index.html`);
59 | mainWindow.webContents.on("did-finish-load", () => {
60 | mainWindow.show();
61 | // mainWindow.webContents.openDevTools();
62 | });
63 | mainWindow.on("closed", (e: any) => {
64 | mainWindow = null;
65 | });
66 | }
67 | app.on("ready", createWindow);
68 | app.on("window-all-closed", () => {
69 | if (process.platform !== "darwin") {
70 | app.quit();
71 | }
72 | });
73 | app.on("activate", () => {
74 | if (mainWindow === null) {
75 | createWindow();
76 | }
77 | });
78 |
--------------------------------------------------------------------------------
/src/infrastructure.ts:
--------------------------------------------------------------------------------
1 | // infrastructure.ts - Common interfaces used by Toby's server.
2 | // Author(s): Frank Hale
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU General Public License
15 | // along with this program. If not, see .
16 |
17 | export interface IVideoGroup {
18 | group: string;
19 | entries: IVideoEntry[];
20 | }
21 |
22 | export interface IVideoEntry {
23 | title: string;
24 | ytid: string;
25 | group?: string;
26 | isArchived?: boolean;
27 | }
28 |
--------------------------------------------------------------------------------
/src/platform.ts:
--------------------------------------------------------------------------------
1 | // platform.js - Platform specific code for Toby
2 | // Author(s): Frank Hale
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU General Public License
15 | // along with this program. If not, see .
16 |
17 | import * as stream from "stream";
18 | import { spawn, ChildProcess } from "child_process";
19 | import * as _ from "lodash";
20 | import * as request from "request";
21 | import AppConfig from "./config";
22 |
23 | const titleCase = require("title-case");
24 | const pkgJSON = require("../package.json");
25 | // const ioHook = require("iohook");
26 |
27 | class Platform {
28 | private node: ChildProcess;
29 | private $content: JQuery;
30 | private $webview: JQuery;
31 | private webview: any;
32 | private snapToPlayerCodeBlock: string;
33 | private socket: SocketIO.Server;
34 | private serverLog: String[];
35 |
36 | static bootstrap() {
37 | return new Platform();
38 | }
39 |
40 | constructor() {
41 | this.node = spawn(
42 | ".\\node_modules\\node\\bin\\node.exe",
43 | ["./build/server.js"],
44 | {
45 | cwd: process.cwd()
46 | }
47 | );
48 | this.$content = $("#content");
49 | this.$webview = $("#webview");
50 | this.webview = this.$webview[0];
51 | this.serverLog = [];
52 |
53 | document.title = pkgJSON.title;
54 |
55 | this.socket = require("socket.io")(AppConfig.socketIOPort);
56 | this.socket.on("connection", s => {
57 | this.$content.append("Socket.IO connection established... ");
58 |
59 | s.on("title", (t: string) => {
60 | if (t !== undefined && t !== "") {
61 | this.$content.append(`setting title to: ${t} `);
62 | document.title = t;
63 | }
64 | });
65 |
66 | s.on("toggle-server-log", () => {
67 | this.f1Handler();
68 | });
69 |
70 | s.on("toggle-fullscreen", () => {
71 | this.f11Handler();
72 | });
73 |
74 | s.on("get-server-log", () => {
75 | s.emit("server-log", { log: this.serverLog });
76 | });
77 |
78 | s.emit("toby-version", {
79 | title: pkgJSON.title,
80 | version: `${titleCase(pkgJSON.name)}-${pkgJSON.version}`
81 | });
82 | });
83 |
84 | this.snapToPlayerCodeBlock = `var actualCode = '(' + function() {
85 | snapToPlayer();
86 | } + ')();';
87 | var script = document.createElement('script');
88 | script.textContent = actualCode;
89 | (document.head||document.documentElement).appendChild(script);
90 | script.parentNode.removeChild(script);
91 | `;
92 |
93 | this.redirectOutput(this.node.stdout);
94 | this.redirectOutput(this.node.stderr);
95 |
96 | let checkServerRunning = setInterval(() => {
97 | request(AppConfig.serverURL, (error, response, body) => {
98 | if (!error && response.statusCode === 200) {
99 | this.$webview.attr("src", AppConfig.serverURL);
100 | $("#loading").css("display", "none");
101 | this.$webview.css("display", "block");
102 | clearInterval(checkServerRunning);
103 | }
104 | });
105 | }, 1000);
106 | this.setup();
107 | }
108 | private setup(): void {
109 | key("f1", this.f1Handler);
110 | key("f11", this.f11Handler);
111 |
112 | if (navigator.userAgent.indexOf("node-webkit") > -1) {
113 | let win = nw.Window.get();
114 |
115 | win.on("loaded", () => {
116 | // win.showDevTools();
117 | win.show();
118 | });
119 |
120 | win.on("restore", () => {
121 | this.webview.executeScript({ code: this.snapToPlayerCodeBlock });
122 | });
123 |
124 | win.on("new-win-policy", (_frame, _url, policy) => {
125 | policy.ignore();
126 | });
127 |
128 | win.on("close", () => {
129 | win.hide();
130 |
131 | this.$webview.remove();
132 |
133 | $.ajax({
134 | type: "POST",
135 | url: "/api/app/close",
136 | async: false
137 | });
138 |
139 | win.close(true);
140 | });
141 |
142 | this.webview.addEventListener(
143 | "newwindow",
144 | this.newWindowHandler.bind(this)
145 | );
146 | }
147 |
148 | window.addEventListener("resize", e => {
149 | this.resizeContent();
150 | });
151 |
152 | this.resizeContent();
153 |
154 | if (
155 | navigator.userAgent.indexOf("node-webkit") > -1 ||
156 | navigator.userAgent.indexOf("Electron") > -1
157 | ) {
158 | this.webview.addEventListener("permissionrequest", (e: any) => {
159 | if (e.permission === "fullscreen") {
160 | e.request.allow();
161 | }
162 | });
163 | }
164 |
165 | if (navigator.userAgent.indexOf("Electron") > -1) {
166 | this.webview.addEventListener(
167 | "new-window",
168 | this.newWindowHandler.bind(this)
169 | );
170 |
171 | window.addEventListener("beforeunload", () => {
172 | $.ajax({
173 | type: "POST",
174 | url: "/api/app/close",
175 | async: false
176 | });
177 | });
178 |
179 | // this.webview.addEventListener("dom-ready", () => {
180 | // this.webview.openDevTools();
181 | // });
182 |
183 | let browserWindow = require("electron").remote.getCurrentWindow();
184 |
185 | this.webview.addEventListener("enter-html-full-screen", () => {
186 | if (!browserWindow.isFullScreen()) {
187 | browserWindow.setFullScreen(true);
188 | }
189 | });
190 |
191 | browserWindow.on("leave-full-screen", () => {
192 | this.webview.executeJavaScript(this.snapToPlayerCodeBlock);
193 | });
194 | }
195 | }
196 | private resizeContent(): void {
197 | this.$content.css("width", window.innerWidth - 20);
198 | this.$content.css("height", window.innerHeight - 20);
199 | }
200 | private strip(s: string): string {
201 | // regex from: http://stackoverflow.com/a/29497680/170217
202 | return s.replace(
203 | /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
204 | ""
205 | );
206 | }
207 | private redirectOutput(x: stream.Readable): void {
208 | let lineBuffer = "";
209 |
210 | x.on("data", data => {
211 | lineBuffer += data.toString();
212 | let lines = lineBuffer.split("\n");
213 |
214 | _.forEach(lines, l => {
215 | if (l !== "") {
216 | // console.log(this.strip(l));
217 | let strippedData = this.strip(l);
218 | this.$content.append(`${strippedData} `);
219 | this.serverLog.push(strippedData);
220 | }
221 | });
222 |
223 | lineBuffer = lines[lines.length - 1];
224 | });
225 | }
226 | private newWindowHandler(e: any): void {
227 | // Looks like we can differentiate between clicking the YouTube icon
228 | // in the player where we want it to open an external browser and clicking
229 | // a suggested video link after a video is played.
230 | //
231 | // When clicking the YouTube link "time_continue" is present in the url.
232 | // {url: "https://www.youtube.com/watch?time_continue=1&v=ctrZdbExVrk"}
233 | //
234 | // When clicking on a suggested video the link is just an ordinary YouTube
235 | // video link with video ID.
236 | // {url: "https://www.youtube.com/watch?v=4nYMdMtGsPo"}
237 |
238 | // NOTE: What I said above is only partially true, the video has to start
239 | // playing for the time_continue to be present in the URL. You cannot
240 | // click the YouTube link and have it open an external browser if the
241 | // video has not started to play.
242 |
243 | e.preventDefault();
244 |
245 | const url = e.targetUrl || e.url;
246 |
247 | if (url.indexOf("?v=") > -1) {
248 | // the id extraction is almost verbatim from:
249 | // http://stackoverflow.com/a/3452617/170217
250 | let video_id = url.split("v=")[1];
251 | let ampersandPosition = video_id.indexOf("&");
252 | if (ampersandPosition !== -1) {
253 | video_id = video_id.substring(0, ampersandPosition);
254 | }
255 | // ------------------------------------------
256 |
257 | this.$content.append(`emitting: play-video for ${video_id} `);
258 | this.socket.emit("play-video", video_id);
259 | } else {
260 | if (navigator.userAgent.indexOf("node-webkit") > -1) {
261 | nw.Shell.openExternal(url);
262 | } else if (navigator.userAgent.indexOf("Electron") > -1) {
263 | const { shell } = require("electron");
264 | shell.openExternal(url);
265 | }
266 | }
267 | }
268 | private f1Handler(): void {
269 | let $content = $("#content"),
270 | $webview = $("#webview");
271 |
272 | if ($content.css("visibility") === "hidden") {
273 | $content.css("visibility", "visible");
274 | $webview.css("visibility", "hidden");
275 | } else {
276 | $content.css("visibility", "hidden");
277 | $webview.css("visibility", "visible");
278 | }
279 | }
280 | private f11Handler(): void {
281 | if (navigator.userAgent.indexOf("node-webkit") > -1) {
282 | let win = nw.Window.get();
283 |
284 | if (win.isFullscreen) {
285 | win.leaveFullscreen();
286 | } else {
287 | win.enterFullscreen();
288 | }
289 | } else if (navigator.userAgent.indexOf("Electron") > -1) {
290 | const browserWindow = require("electron").remote.getCurrentWindow();
291 |
292 | if (browserWindow.isFullScreen()) {
293 | browserWindow.setFullScreen(false);
294 | this.webview.executeJavaScript(this.snapToPlayerCodeBlock);
295 | } else {
296 | browserWindow.setFullScreen(true);
297 | }
298 | }
299 | }
300 | }
301 |
302 | $(document).ready(function() {
303 | Platform.bootstrap();
304 | });
305 |
--------------------------------------------------------------------------------
/src/react-components/command-input-ui.tsx:
--------------------------------------------------------------------------------
1 | // command-input-ui.tsx - The command line input component for Toby
2 | // Author(s): Frank Hale
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU General Public License
15 | // along with this program. If not, see .
16 |
17 | import * as React from "react";
18 | import * as _ from "lodash";
19 | import * as $ from "jquery";
20 |
21 | enum Keys {
22 | Enter = 13,
23 | Up = 38,
24 | Down = 40
25 | }
26 |
27 | interface ICommandInputState {
28 | commandIndex?: number;
29 | commandsEntered?: string[];
30 | commandText?: JQuery;
31 | }
32 |
33 | export interface ICommandInputProps {
34 | onKeyEnter?: (event: any) => void;
35 | onKeyChanged?: (event: any) => void;
36 | placeHolder: string;
37 | }
38 |
39 | export class CommandInput extends React.Component<
40 | ICommandInputProps,
41 | ICommandInputState
42 | > {
43 | constructor(props: any) {
44 | super(props);
45 |
46 | this.onCommandInputKeyUp = this.onCommandInputKeyUp.bind(this);
47 | this.onCommandInputChanged = this.onCommandInputChanged.bind(this);
48 |
49 | this.state = {
50 | commandIndex: -1,
51 | commandsEntered: []
52 | };
53 | }
54 | componentDidMount() {
55 | const $commandText = $("#commandText"),
56 | resizeCommandInput = (): void => {
57 | $commandText.width(window.innerWidth - 50);
58 | };
59 |
60 | $(window).resize(e => {
61 | resizeCommandInput();
62 | });
63 |
64 | resizeCommandInput();
65 |
66 | this.setState({ commandText: $commandText });
67 | }
68 | private onCommandInputKeyUp(e: any): void {
69 | if (e.which === Keys.Up) {
70 | let commandIndex =
71 | this.state.commandIndex === -1
72 | ? this.state.commandsEntered.length - 1
73 | : this.state.commandIndex - 1;
74 |
75 | if (commandIndex < 0) {
76 | commandIndex = 0;
77 | }
78 |
79 | this.setState({ commandIndex: commandIndex }, () => {
80 | this.state.commandText.val(this.state.commandsEntered[commandIndex]);
81 | });
82 | } else if (e.which === Keys.Down) {
83 | let commandIndex =
84 | this.state.commandIndex === -1 ? 0 : this.state.commandIndex + 1;
85 |
86 | if (commandIndex > this.state.commandsEntered.length) {
87 | commandIndex = this.state.commandsEntered.length;
88 | }
89 |
90 | this.setState({ commandIndex: commandIndex }, () => {
91 | this.state.commandText.val(this.state.commandsEntered[commandIndex]);
92 | });
93 | } else if (e.which === Keys.Enter) {
94 | const textEntered = this.state.commandText.val() as string;
95 | if (!(textEntered.length > 0)) return;
96 |
97 | this.setState(
98 | {
99 | commandsEntered: _.uniq(
100 | this.state.commandsEntered.concat([textEntered])
101 | ),
102 | commandIndex: -1
103 | },
104 | () => {
105 | if (this.props.onKeyEnter !== undefined) {
106 | this.props.onKeyEnter(textEntered);
107 | }
108 | }
109 | );
110 | }
111 | }
112 | private onCommandInputChanged(e: any): void {
113 | if (this.props.onKeyChanged !== undefined) {
114 | this.props.onKeyChanged(this.state.commandText.val());
115 | }
116 | }
117 | render() {
118 | return (
119 |
120 | >
129 |
130 | );
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/react-components/dropdown-ui.tsx:
--------------------------------------------------------------------------------
1 | // dropdown-ui.tsx
2 | // Author(s): Frank Hale
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU General Public License
15 | // along with this program. If not, see .
16 |
17 | import * as React from "react";
18 | import * as $ from "jquery";
19 | import * as _ from "lodash";
20 |
21 | export interface IDropDownItem {
22 | name: string;
23 | value: string;
24 | action(): void;
25 | }
26 |
27 | interface IDropDownProps {
28 | name: string;
29 | items: IDropDownItem[];
30 | onDropDownChange?: (value: string, id: JQuery) => void;
31 | disabled?: boolean;
32 | selected?: string;
33 | style?: {};
34 | className?: string;
35 | }
36 |
37 | interface IDropDownState {
38 | name?: string;
39 | items?: IDropDownItem[];
40 | onDropDownChange?: (value: string, id: JQuery) => void;
41 | disabled?: boolean;
42 | selected?: string;
43 | }
44 |
45 | export class DropDown extends React.Component {
46 | constructor(props: any) {
47 | super(props);
48 |
49 | this.onDropDownChange = this.onDropDownChange.bind(this);
50 |
51 | this.state = {
52 | name: "",
53 | items: []
54 | };
55 | }
56 |
57 | static getDerivedStateFromProps(
58 | props: IDropDownProps,
59 | state: IDropDownState
60 | ): IDropDownState {
61 | if (!(_.isEmpty(props.name) && _.isEmpty(props.items))) {
62 | return {
63 | name: props.name,
64 | items: props.items,
65 | disabled: props.disabled === undefined ? false : true,
66 | selected: props.selected,
67 | onDropDownChange:
68 | props.onDropDownChange !== undefined
69 | ? props.onDropDownChange
70 | : () => {}
71 | };
72 | }
73 |
74 | return null;
75 | }
76 |
77 | private onDropDownChange(e: any): void {
78 | if (this.state.onDropDownChange !== undefined) {
79 | this.state.onDropDownChange(e.target.value, $(e.target).prop("id"));
80 | }
81 | }
82 | render() {
83 | let renderedItems = _.map(this.state.items, (e: any, i) => {
84 | return (
85 |
88 | );
89 | });
90 |
91 | return (
92 |
103 | );
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/react-components/infrastructure.ts:
--------------------------------------------------------------------------------
1 | // infrastructure.ts - Miscellaneous interfaces and/or other things needed by
2 | // multiple files in Toby
3 | // Author(s): Frank Hale
4 | //
5 | // This program is free software: you can redistribute it and/or modify
6 | // it under the terms of the GNU General Public License as published by
7 | // the Free Software Foundation, either version 3 of the License, or
8 | // (at your option) any later version.
9 | //
10 | // This program is distributed in the hope that it will be useful,
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | // GNU General Public License for more details.
14 | //
15 | // You should have received a copy of the GNU General Public License
16 | // along with this program. If not, see .
17 |
18 | export interface IVideoGroup {
19 | group: string;
20 | entries: IVideoEntry[];
21 | }
22 |
23 | export interface IVideoEntry {
24 | title: string;
25 | ytid: string;
26 | group?: string;
27 | isArchived?: boolean;
28 | justAdded?: boolean;
29 | }
30 |
31 | export interface ITobyVersionInfo {
32 | title: string;
33 | version: string;
34 | }
35 |
36 | export interface ISearchResults {
37 | playVideo: (video: IVideoEntry, data: IVideoGroup[]) => void;
38 | title: string;
39 | ytid: string;
40 | group: string;
41 | thumbnail: string;
42 | isArchived: boolean;
43 | justAdded?: boolean;
44 | }
45 |
--------------------------------------------------------------------------------
/src/react-components/server-log-ui.tsx:
--------------------------------------------------------------------------------
1 | // server-log-ui.tsx - Server log React component for Toby
2 | // Author(s): Frank Hale
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU General Public License
15 | // along with this program. If not, see .
16 |
17 | import * as React from "react";
18 | import * as _ from "lodash";
19 |
20 | export interface IServerLogProps {
21 | display: boolean;
22 | log: String[];
23 | }
24 |
25 | interface IServerLogState {
26 | display: boolean;
27 | log: String[];
28 | }
29 |
30 | export class ServerLog extends React.Component<
31 | IServerLogProps,
32 | IServerLogState
33 | > {
34 | constructor(props: IServerLogProps) {
35 | super(props);
36 |
37 | this.state = {
38 | display: false,
39 | log: []
40 | };
41 | }
42 |
43 | static getDerivedStateFromProps(
44 | props: IServerLogProps,
45 | state: IServerLogState
46 | ): IServerLogState {
47 | if (props.display !== undefined && props.log !== undefined) {
48 | return {
49 | display: props.display,
50 | log: props.log
51 | };
52 | }
53 |
54 | return null;
55 | }
56 |
57 | render() {
58 | if (this.state.display && !_.isEmpty(this.state.log)) {
59 | return (
60 |
466 | );
467 | }
468 | }
469 |
470 | $(document).ready(() => {
471 | ReactDOM.render(, document.getElementById("ui"));
472 | });
473 |
--------------------------------------------------------------------------------
/src/react-components/version-ui.tsx:
--------------------------------------------------------------------------------
1 | // version-ui.tsx - Version info React component for Toby
2 | // Author(s): Frank Hale
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU General Public License
15 | // along with this program. If not, see .
16 |
17 | import * as React from "react";
18 | import * as _ from "lodash";
19 |
20 | export interface IVersionProps {
21 | display: boolean;
22 | info: string;
23 | }
24 |
25 | interface IVersionState {
26 | display: boolean;
27 | info: string;
28 | }
29 |
30 | export class Version extends React.Component {
31 | constructor(props: IVersionProps) {
32 | super(props);
33 |
34 | this.state = {
35 | display: false,
36 | info: ""
37 | };
38 | }
39 |
40 | static getDerivedStateFromProps(
41 | props: IVersionProps,
42 | state: IVersionState
43 | ): IVersionState {
44 | if (props.display !== undefined && !_.isEmpty(props.info)) {
45 | return {
46 | display: props.display,
47 | info: props.info
48 | };
49 | }
50 |
51 | return null;
52 | }
53 |
54 | render() {
55 | if (this.state.display && !_.isEmpty(this.state.info)) {
56 | return
{this.state.info}
;
57 | }
58 |
59 | return null;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/react-components/video-list-grid-ui.tsx:
--------------------------------------------------------------------------------
1 | // video-list-grid-ui.tsx - A video list grid React component for Toby
2 | // Author(s): Frank Hale
3 | //
4 | // This program is free software: you can redistribute it and/or modify
5 | // it under the terms of the GNU General Public License as published by
6 | // the Free Software Foundation, either version 3 of the License, or
7 | // (at your option) any later version.
8 | //
9 | // This program is distributed in the hope that it will be useful,
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | // GNU General Public License for more details.
13 | //
14 | // You should have received a copy of the GNU General Public License
15 | // along with this program. If not, see .
16 |
17 | import * as React from "react";
18 | import * as _ from "lodash";
19 |
20 | import { ISearchResults } from "./infrastructure";
21 |
22 | export interface IViewListGridProps {
23 | data: ISearchResults[];
24 | applyFilter: string;
25 | }
26 |
27 | interface IViewListGridState {
28 | data?: ISearchResults[];
29 | applyFilter?: string;
30 | }
31 |
32 | export class VideoListGrid extends React.Component<
33 | IViewListGridProps,
34 | IViewListGridState
35 | > {
36 | constructor(props: any) {
37 | super(props);
38 |
39 | this.state = {
40 | data: [],
41 | applyFilter: ""
42 | };
43 | }
44 |
45 | static getDerivedStateFromProps(
46 | props: IViewListGridProps,
47 | state: IViewListGridState
48 | ): IViewListGridState {
49 | let videos: ISearchResults[] = [];
50 |
51 | if (!_.isEmpty(props.data)) {
52 | videos = props.data.map((d, i) => {
53 | return {
54 | playVideo: d.playVideo,
55 | title: d.title,
56 | ytid: d.ytid,
57 | group: d.group,
58 | thumbnail: d.thumbnail,
59 | isArchived: d.isArchived
60 | };
61 | });
62 |
63 | return {
64 | data: videos,
65 | applyFilter: props.applyFilter || ""
66 | };
67 | }
68 |
69 | return null;
70 | }
71 |
72 | render() {
73 | return (
74 |