diff --git a/README.md b/README.md index e1d6cf9..311a9f9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ ## liveport - live broadcast support tool. -したらば掲示板の読み込み、ブラウザ上に表示・読み上げ(v0.0.1) +したらば掲示板・CaveTubeのコメントを、ブラウザ上に表示・読み上げ(v1.0.0) ### 使い方 * liveport.exeを起動 * OBS Studioの設定 @@ -17,10 +17,12 @@ * background-color: rgba(0, 0, 0, 0); 背景透過(必須) * overflow: hidden; 長い文章を途中で省略する(必須) * color:white; 文字の色(変更可能) -* URLを入力後、**[▶]読み込み開始**ボタンを押す。 +* タイトルバーにURLを入力後、**[▶]読み込み開始**ボタンを押す。 * 詳細な設定は[resources/app/config.json] +* データは以下に保存される + * C:\Users\%USERNAME%\AppData\Roaming\liveport\ + * - -* ver 0.0.1 +* ver 1.0.0 * 初版 diff --git a/package.json b/package.json index a810462..354aafd 100644 --- a/package.json +++ b/package.json @@ -1,75 +1,75 @@ { - "name": "liveport", - "version": "0.0.1", - "description": "livestream support tool", - "main": "build/main.js", - "scripts": { - "watch": "webpack-dashboard -- webpack -d --watch", - "start": "webpack-dashboard -- webpack-dev-server -d --hot --inline", - "build": "electron-packager . live_gen --platform=win32 --arch=x64 --version=1.4.6 --out ./release" - }, - "author": "https://github.com/odangosan", - "license": "ISC", - "devDependencies": { - "@types/electron": "^1.4.25", - "@types/express": "^4.0.33", - "@types/iconv-lite": "0.0.1", - "@types/material-design-lite": "^1.1.14", - "@types/materialize-css": "^0.97.32", - "@types/node": "^6.0.46", - "@types/request": "0.0.33", - "@types/request-promise": "^4.1.33", - "@types/socket.io": "^1.4.27", - "@types/socket.io-client": "^1.4.28", - "@types/webspeechapi": "0.0.27", - "autoprefixer": "^6.5.3", - "copy-webpack-plugin": "^4.0.0", - "css-loader": "^0.25.0", - "electron": "^1.4.5", - "electron-connect": "^0.6.0", - "electron-packager": "^8.2.0", - "exports-loader": "^0.6.3", - "file-loader": "^0.9.0", - "font-awesome": "^4.7.0", - "github": "^7.0.0", - "gulp": "^3.9.1", - "gulp-rename": "^1.2.2", - "gulp-typescript": "^3.1.2", - "gulp-useref": "^3.1.2", - "gulp-util": "^3.0.7", - "gulp-webpack": "^1.5.0", - "gulp-zip": "^3.2.0", - "iconv-lite": "^0.4.13", - "imports-loader": "^0.6.5", - "jquery": "^3.1.1", - "json-loader": "^0.5.4", - "less": "^2.7.1", - "less-loader": "^2.2.3", - "material-design-icons": "^3.0.1", - "material-design-lite": "^1.2.1", - "open": "0.0.5", - "postcss-import": "^8.2.0", - "postcss-loader": "^1.1.1", - "request": "^2.78.0", - "request-promise": "^4.1.1", - "resolve-url-loader": "^1.6.0", - "run-sequence": "^1.2.2", - "script-loader": "^0.7.0", - "socket.io-client": "^1.5.1", - "style-loader": "^0.13.1", - "ts-loader": "^1.0.0", - "typescript": "^2.0.8", - "typings": "^1.5.0", - "url-loader": "^0.5.7", - "vue": "^2.0.5", - "vue-typed": "^2.0.1", - "webpack": "^1.13.3", - "webpack-dashboard": "^0.2.0", - "webpack-dev-server": "^1.16.2", - "webpack-merge": "^0.15.0" - }, - "dependencies": { - "express": "^4.14.0", - "socket.io": "^1.5.1" - } -} + "name": "liveport", + "version": "1.0.0", + "description": "livestream support tool", + "main": "build/main.js", + "scripts": { + "watch": "webpack-dashboard -- webpack -d --watch", + "start": "webpack-dashboard -- webpack-dev-server -d --hot --inline", + "build": "electron-packager . live_gen --platform=win32 --arch=x64 --version=1.4.6 --out ./release" + }, + "author": "https://github.com/odangosan", + "license": "ISC", + "devDependencies": { + "@types/electron": "^1.4.25", + "@types/express": "^4.0.33", + "@types/iconv-lite": "0.0.1", + "@types/material-design-lite": "^1.1.14", + "@types/materialize-css": "^0.97.32", + "@types/node": "^6.0.46", + "@types/request": "0.0.33", + "@types/request-promise": "^4.1.33", + "@types/socket.io": "^1.4.27", + "@types/socket.io-client": "^1.4.28", + "@types/webspeechapi": "0.0.27", + "autoprefixer": "^6.5.3", + "copy-webpack-plugin": "^4.0.0", + "css-loader": "^0.25.0", + "electron": "^1.4.5", + "electron-connect": "^0.6.0", + "electron-packager": "^8.2.0", + "exports-loader": "^0.6.3", + "file-loader": "^0.9.0", + "font-awesome": "^4.7.0", + "github": "^7.0.0", + "gulp": "^3.9.1", + "gulp-rename": "^1.2.2", + "gulp-typescript": "^3.1.2", + "gulp-useref": "^3.1.2", + "gulp-util": "^3.0.7", + "gulp-webpack": "^1.5.0", + "gulp-zip": "^3.2.0", + "iconv-lite": "^0.4.13", + "imports-loader": "^0.6.5", + "jquery": "^3.1.1", + "json-loader": "^0.5.4", + "less": "^2.7.1", + "less-loader": "^2.2.3", + "material-design-icons": "^3.0.1", + "material-design-lite": "^1.2.1", + "open": "0.0.5", + "postcss-import": "^8.2.0", + "postcss-loader": "^1.1.1", + "request": "^2.78.0", + "request-promise": "^4.1.1", + "resolve-url-loader": "^1.6.0", + "run-sequence": "^1.2.2", + "script-loader": "^0.7.0", + "socket.io-client": "^1.5.1", + "style-loader": "^0.13.1", + "ts-loader": "^1.0.0", + "typescript": "^2.0.8", + "typings": "^1.5.0", + "url-loader": "^0.5.7", + "vue": "^2.0.5", + "vue-typed": "^2.0.1", + "webpack": "^1.13.3", + "webpack-dashboard": "^0.2.0", + "webpack-dev-server": "^1.16.2", + "webpack-merge": "^0.15.0" + }, + "dependencies": { + "express": "^4.14.0", + "socket.io": "^1.5.1" + } +} \ No newline at end of file diff --git a/src/renderer/ts/Application.ts b/src/renderer/ts/Application.ts index 6ff69ac..7536528 100644 --- a/src/renderer/ts/Application.ts +++ b/src/renderer/ts/Application.ts @@ -1,7 +1,9 @@ "use strict" import * as Vue from "Vue"; import { Component, Watch } from "vue-typed" -import { Thread } from "./Thread"; +import { DataSource } from "./DataSource"; +import { Shitaraba } from "./Shitaraba"; +import { CaveTube } from "./CaveTube"; import { VOICE, VoiceParameter } from "./Voice" import StringUtil from "./StringUtil"; import Logger from "./Logger"; @@ -19,13 +21,13 @@ export default class Application extends Vue { testMessage: string = 'このテキストはテストメッセージです'; url: string = ""; processing: boolean = false; - thread: Thread; + thread: DataSource; constructor() { super(); Logger.log("start", "hello application."); this.pManager = new ProvideManager(); this.setTitle(""); - this.thread = new Thread(); + this.thread = new Shitaraba("dummy"); this.loadSettings(); } @@ -127,50 +129,56 @@ export default class Application extends Vue { } start() { - if (!this.validate()) return; this.processing = true; - this.loadUrl(); + if (!this.validate()) { + this.processing = false; + return; + } + if (this.thread) { + if (this.url != this.thread.url) { + this.loadUrlSource(); + } + } else { + this.loadUrlSource(); + } this.startThreadRequest(); this.startProvide(); } + validate(): boolean { - if (this.pManager.voice === VOICE.SOFTALK && this.path === "") { + if (this.pManager.voice === VOICE.SOFTALK && this.path === "" && this.pManager.reading && this.processing === true) { let warn = { - message: "ERROR : pathが設定されていません。", - timeout: 3000 + message: "ERROR : pathが設定されていません。" } this.snackbar(warn); return false; } - return true; - } - loadUrl() { - if (this.url != this.thread.url) { - this.initUrlSource(); - Logger.log("change", "modified thread url."); + if (!this.isValidURL()) { + Logger.log("invalid url", "not supported url."); + return false; } + return true; } - initUrlSource() { - this.thread = new Thread(this.url); - } + // refresh requestOnce() { - if (!this.url) { - Logger.log("invalid url", "no input."); + if (!this.validate()) { return; } - if (!Thread.isShitarabaURL(this.url)) { - Logger.log("invalid url", "not shitaraba url."); - return; - } - this.thread = new Thread(this.url); + this.loadUrlSource(false); + this.thread.request( (newArrival: number) => { Logger.log("request success", newArrival.toString()); }, (err: any) => { Logger.log("request failed", err); + let warn = { + message: "ERROR : " + err, + timeout: 6000 + } + this.snackbar(warn); } ); } @@ -217,10 +225,10 @@ export default class Application extends Vue { // computed get validUrl() { - return Thread.isShitarabaURL(this.url); + return this.isValidURL(); } - snackbar(data: { message: string, timeout: number } = { message: "info", timeout: 3000 }) { + snackbar(data: { message: string, timeout?: number } = { message: "info", timeout: 3000 }) { var snackbarContainer: any = document.querySelector('#demo-snackbar-example'); // var handler = function (event) { // Logger.log("snackbar", ""); @@ -304,7 +312,8 @@ export default class Application extends Vue { try { this.url = settings.url; if (this.url) { - this.initUrlSource(); + if (this.isValidURL()) + this.loadUrlSource(); this.setTitle(this.thread.title); }; this.autoScroll = Boolean(settings.autoScroll); @@ -354,4 +363,29 @@ export default class Application extends Vue { var settings = JSON.parse(localStorage.getItem(SETTINGS)); } version = VERSION; + + + isValidURL(): boolean { + if (!this.url) { + Logger.log("invalid url", "no input."); + return false; + } + return Shitaraba.isValidURL(this.url) || CaveTube.isValidURL(this.url); + } + + // allocate + loadUrlSource(load: boolean = true) { + if (Shitaraba.isValidURL(this.url)) { + this.thread = new Shitaraba(this.url); + if (load) { + this.thread.load(); + } + } + if (CaveTube.isValidURL(this.url)) { + this.thread = new CaveTube(this.url); + if (load) { + this.thread.load(); + } + } + } } diff --git a/src/renderer/ts/CaveTube.ts b/src/renderer/ts/CaveTube.ts new file mode 100644 index 0000000..43d7d7c --- /dev/null +++ b/src/renderer/ts/CaveTube.ts @@ -0,0 +1,107 @@ +"use strict" +import * as rp from "request-promise"; +import * as iconv from "iconv-lite"; +import Message from "./Message"; +import { DataSource } from "./DataSource"; +const CAVETUBE_REGEX = new RegExp(/https:\/\/www\.cavelis\.net\/live\/(.+)/); +const DK = "46CBA895366C49938CDEC4308E0DFE6B"; + +export class CaveTube extends DataSource { + stream_name: string = ""; + request(success: (boolean) => void, failed: (err: any) => void) { + if (!this.url) { + failed("url is not set"); + return; + } + if (!CaveTube.isValidURL(this.url)) { + failed("not CaveTube url"); + return; + } + if (this.stream_name) this.datRequest(success, failed); + else this.streamNameRequest(success, failed); + + } + + datRequest(success: (boolean) => void, failed: (err: any) => void) { + var commentUrl = "http://ws.cavelis.net:3000/comment/" + this.stream_name + "?devkey=" + DK; + console.log("request comment url : " + commentUrl); + return rp({ url: commentUrl, timeout: 8000 }) + .then((json) => { + console.log("request result : ok!"); + let NewArrivals = this.data2json(json); + success(NewArrivals); + }) + .catch((err) => { + console.log("error..."); + console.log(err); + failed(err); + }); + } + + streamNameRequest(success: (boolean) => void, failed: (err: any) => void) { + var matches = this.url.match(CAVETUBE_REGEX); + var streamNameUrl = `https://www.cavelis.net/api/live_url/${matches[1]}`; + console.log("request stream_name url : " + streamNameUrl); + rp({ url: streamNameUrl, timeout: 8000 }) + .then((json) => { + return this.stream_name = JSON.parse(json).stream_name; + }).then(() => { + if (!this.stream_name) { + failed("this stream is ended"); + } + console.log("stream_name result : ok!"); + return this.datRequest(success, failed); + }) + .catch((err) => { + console.log("error..."); + console.log(err); + }); + } + // 新着レスが有る場合はその数を返す + data2json(data: string): number { + let comments = JSON.parse(data).comments; + /* + 末尾に改行コードがついているので、 + line.lengthは取得したレス+1となっている。 + そのため-1 + */ + var resArray: Message[] = []; + for (var i in comments) { + if (+i+1 > this.messages.length) { + var res = new Message(); + res.num = comments[i].comment_num; + res.name = comments[i].name; + res.mail = ""; + res.date = this.calcDatatime(comments[i].time); + res.text = comments[i].message; + res.id = ""; + resArray.push(res); + } + } + this.title = this.url; + console.log("new thread title : " + this.title); + this.messages = this.messages.concat(resArray); + this.messages.filter((x, i, self) => self.indexOf(x) === i); + this.sortMessage(); + // CaveTube may change ID each time + // this.save(); + return resArray.length; + } + + calcDatatime(ux: number) { + var d = new Date(ux); + var year = d.getFullYear(); + var month = d.getMonth() + 1; + var day = d.getDate(); + var hour = (d.getHours() < 10) ? '0' + d.getHours() : d.getHours(); + var min = (d.getMinutes() < 10) ? '0' + d.getMinutes() : d.getMinutes(); + var sec = (d.getSeconds() < 10) ? '0' + d.getSeconds() : d.getSeconds(); + return `${year}/${month}/${day} ${hour}:${min}:${sec}`; + } + + static isValidURL(url: string): boolean { + return CAVETUBE_REGEX.test(url); + } +} + +export default CaveTube; diff --git a/src/renderer/ts/DataSource.ts b/src/renderer/ts/DataSource.ts index df1dbe0..79596e1 100644 --- a/src/renderer/ts/DataSource.ts +++ b/src/renderer/ts/DataSource.ts @@ -1,16 +1,13 @@ import Message from "./Message" -abstract class DataSource { +export abstract class DataSource { messages: Message[] = []; url: string = ""; bookmark: number = 0; title: string = ""; - constructor(url?: string) { - if (url) { - this.url = url; - this.dataSourceFactory(this.url); - } + constructor(url: string) { + this.url = url; } - + abstract request(success: (boolean) => void, failed: (err: any) => void); abstract data2json(data: string): number; @@ -28,16 +25,17 @@ abstract class DataSource { this.save(); } - dataSourceFactory(url: string) { - var thread = DataSource.loadDataSource(url); + load(): boolean { + var thread = DataSource.loadDataSource(this.url); if (thread == null) { console.log("new thread.") - return; + return false; } console.log("read thread from localstorage.") this.decodeFromJson(thread); + return true; } - + decodeFromJson(data: any) { var data = JSON.parse(data); this.bookmark = data.bookmark; @@ -82,5 +80,3 @@ abstract class DataSource { localStorage.clear(); } } - -export default DataSource; \ No newline at end of file diff --git a/src/renderer/ts/Thread.ts b/src/renderer/ts/Shitaraba.ts similarity index 89% rename from src/renderer/ts/Thread.ts rename to src/renderer/ts/Shitaraba.ts index d04ecc8..ef7be66 100644 --- a/src/renderer/ts/Thread.ts +++ b/src/renderer/ts/Shitaraba.ts @@ -2,19 +2,18 @@ import * as rp from "request-promise"; import * as iconv from "iconv-lite"; import Message from "./Message"; -import DataSource from "./DataSource"; -const URL = "http://jbbs.shitaraba.net/bbs/read.cgi/netgame/12802/1478775754/"; +import {DataSource} from "./DataSource"; const SHITARABA_REGEX = new RegExp(/http:\/\/jbbs.shitaraba.net\/bbs\/read.cgi\/(\w+)\/(\d+)\/(\d+)\/.*/); const RES_SPLITTER = new RegExp(/<>/g); const NEWLINE_SPLITTER = new RegExp(/\n/g); -export class Thread extends DataSource { +export class Shitaraba extends DataSource { request(success: (boolean) => void, failed: (err: any) => void) { if (!this.url) { failed("url is not set"); return; } - if (!Thread.isShitarabaURL(this.url)) { + if (!Shitaraba.isValidURL(this.url)) { failed("not shitaraba url"); return; } @@ -65,9 +64,9 @@ export class Thread extends DataSource { return resArray.length; } - static isShitarabaURL(url: string): boolean { + static isValidURL(url: string): boolean { return SHITARABA_REGEX.test(url); } } -export default Thread; +export default Shitaraba; diff --git a/tsconfig.json b/tsconfig.json index 6d9f261..86e47ac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,8 +29,9 @@ "src/renderer/ts/SofTalk.ts", "src/renderer/ts/Speaker.ts", "src/renderer/ts/StringUtil.ts", - "src/renderer/ts/Thread.ts", + "src/renderer/ts/Shitaraba.ts", + "src/renderer/ts/CaveTube.ts", "src/renderer/ts/Voice.ts", "src/renderer/ts/WebSpeechApi.ts" ] -} +} \ No newline at end of file