├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .firebaserc
├── .gitignore
├── .netlify
├── README.md
├── _redirects
├── build
├── build.js
├── check-versions.js
├── dev-client.js
├── dev-server.js
├── utils.js
├── vue-loader.conf.js
├── webpack.base.conf.js
├── webpack.dev.conf.js
└── webpack.prod.conf.js
├── config
├── dev.env.js
├── index.js
└── prod.env.js
├── extension
├── background.js
└── manifest.json
├── firebase.json
├── functions
├── index.js
└── package.json
├── index.html
├── package.json
├── server
├── index.js
├── package.json
├── routes
│ ├── mal.js
│ └── opening.js
└── yarn.lock
├── src
├── App.css
├── App.vue
├── assets
│ ├── emoji
│ │ ├── FrostyFridays.png
│ │ ├── TsuyuW.png
│ │ ├── UmiDorito.png
│ │ └── Yousoro.png
│ ├── umi-login.png
│ └── umi.png
├── components
│ ├── Collection.vue
│ ├── DashboardLoadingSection.vue
│ ├── EpisodeScroller.vue
│ ├── Header.vue
│ ├── LoadingMediaItem.vue
│ ├── MediaItem.vue
│ ├── QueueButton.vue
│ ├── QueueItem.vue
│ ├── Reactotron.vue
│ ├── Search.vue
│ ├── SeriesItem.vue
│ └── Video.vue
├── lib
│ ├── api.js
│ ├── auth.js
│ ├── bif.js
│ ├── cdnRewrite.js
│ ├── clappr-level-selector.js
│ ├── emoji.js
│ ├── firebase.js
│ └── prettyTime.js
├── main.js
├── pages
│ ├── Changelog.vue
│ ├── Dashboard.vue
│ ├── History.vue
│ ├── Login.vue
│ ├── Media.vue
│ ├── Migrate.vue
│ ├── Notice.vue
│ ├── Queue.vue
│ ├── Room.vue
│ ├── Search.vue
│ ├── Series.vue
│ └── Settings.vue
├── router
│ └── index.js
└── store
│ └── index.js
├── static
└── .gitkeep
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", {
4 | "modules": false,
5 | "targets": {
6 | "chrome": 62,
7 | "firefox": 56,
8 | "ios": 11,
9 | "safari": 11,
10 | "edge": 15
11 | },
12 | "loose": true,
13 | "useBuiltIns": true
14 | }]
15 | ],
16 | "comments": false
17 | }
18 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/*.js
2 | config/*.js
3 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // http://eslint.org/docs/user-guide/configuring
2 |
3 | module.exports = {
4 | root: true,
5 | parserOptions: {
6 | parser: 'babel-eslint',
7 | sourceType: 'module'
8 | },
9 | env: {
10 | browser: true,
11 | },
12 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
13 | extends: ['plugin:vue/essential', 'standard'],
14 | // required to lint *.vue files
15 | plugins: [
16 | 'vue'
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "umi-player"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | npm-debug.log
5 | firebase-debug.log
6 | extension.zip
7 |
--------------------------------------------------------------------------------
/.netlify:
--------------------------------------------------------------------------------
1 | {"site_id":"21b5bf8b-865b-459a-ad16-dcdf89e82d67","path":"dist"}
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # umi
2 |
3 | > Better Crunchyroll
4 |
5 | ## Build Setup
6 |
7 | ``` bash
8 | # install dependencies
9 | npm install
10 |
11 | # serve with hot reload at localhost:8080
12 | npm run dev
13 |
14 | # build for production with minification
15 | npm run build
16 |
17 | # build for production and view the bundle analyzer report
18 | npm run build --report
19 | ```
20 |
21 | For detailed explanation on how things work, checkout the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).
22 |
--------------------------------------------------------------------------------
/_redirects:
--------------------------------------------------------------------------------
1 | /cdn/* http://img1.ak.crunchyroll.com/:splat 200
2 | /pl-proxy/* https://pl.crunchyroll.com/:splat 200
3 | /appcache/* /index.html 404
4 | /* /index.html 200
5 |
--------------------------------------------------------------------------------
/build/build.js:
--------------------------------------------------------------------------------
1 | // https://github.com/shelljs/shelljs
2 | require('./check-versions')()
3 |
4 | process.env.NODE_ENV = 'production'
5 |
6 | var ora = require('ora')
7 | var path = require('path')
8 | var chalk = require('chalk')
9 | var shell = require('shelljs')
10 | var webpack = require('webpack')
11 | var config = require('../config')
12 | var webpackConfig = require('./webpack.prod.conf')
13 |
14 | var spinner = ora('building for production...')
15 | spinner.start()
16 |
17 | var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
18 | shell.rm('-rf', assetsPath)
19 | shell.mkdir('-p', assetsPath)
20 | shell.config.silent = true
21 | shell.cp('-R', 'static/*', assetsPath)
22 | shell.config.silent = false
23 |
24 | webpack(webpackConfig, function (err, stats) {
25 | spinner.stop()
26 | if (err) throw err
27 | process.stdout.write(stats.toString({
28 | colors: true,
29 | modules: false,
30 | children: false,
31 | chunks: false,
32 | chunkModules: false
33 | }) + '\n\n')
34 |
35 | console.log(chalk.cyan(' Build complete.\n'))
36 | console.log(chalk.yellow(
37 | ' Tip: built files are meant to be served over an HTTP server.\n' +
38 | ' Opening index.html over file:// won\'t work.\n'
39 | ))
40 | })
41 |
--------------------------------------------------------------------------------
/build/check-versions.js:
--------------------------------------------------------------------------------
1 | var chalk = require('chalk')
2 | var semver = require('semver')
3 | var packageConfig = require('../package.json')
4 |
5 | function exec (cmd) {
6 | return require('child_process').execSync(cmd).toString().trim()
7 | }
8 |
9 | var versionRequirements = [
10 | {
11 | name: 'node',
12 | currentVersion: semver.clean(process.version),
13 | versionRequirement: packageConfig.engines.node
14 | },
15 | {
16 | name: 'npm',
17 | currentVersion: exec('npm --version'),
18 | versionRequirement: packageConfig.engines.npm
19 | }
20 | ]
21 |
22 | module.exports = function () {
23 | var warnings = []
24 | for (var i = 0; i < versionRequirements.length; i++) {
25 | var mod = versionRequirements[i]
26 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
27 | warnings.push(mod.name + ': ' +
28 | chalk.red(mod.currentVersion) + ' should be ' +
29 | chalk.green(mod.versionRequirement)
30 | )
31 | }
32 | }
33 |
34 | if (warnings.length) {
35 | console.log('')
36 | console.log(chalk.yellow('To use this template, you must update following to modules:'))
37 | console.log()
38 | for (var i = 0; i < warnings.length; i++) {
39 | var warning = warnings[i]
40 | console.log(' ' + warning)
41 | }
42 | console.log()
43 | process.exit(1)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/build/dev-client.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | require('eventsource-polyfill')
3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
4 |
5 | hotClient.subscribe(function (event) {
6 | if (event.action === 'reload') {
7 | window.location.reload()
8 | }
9 | })
10 |
--------------------------------------------------------------------------------
/build/dev-server.js:
--------------------------------------------------------------------------------
1 | require('./check-versions')()
2 |
3 | var config = require('../config')
4 | if (!process.env.NODE_ENV) {
5 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
6 | }
7 |
8 | var opn = require('opn')
9 | var path = require('path')
10 | var express = require('express')
11 | var webpack = require('webpack')
12 | var proxyMiddleware = require('http-proxy-middleware')
13 | var webpackConfig = require('./webpack.dev.conf')
14 |
15 | // default port where dev server listens for incoming traffic
16 | var port = process.env.PORT || config.dev.port
17 | // automatically open browser, if not set will be false
18 | var autoOpenBrowser = !!config.dev.autoOpenBrowser
19 | // Define HTTP proxies to your custom API backend
20 | // https://github.com/chimurai/http-proxy-middleware
21 | var proxyTable = config.dev.proxyTable
22 |
23 | var app = express()
24 | var compiler = webpack(webpackConfig)
25 |
26 | var devMiddleware = require('webpack-dev-middleware')(compiler, {
27 | publicPath: webpackConfig.output.publicPath,
28 | quiet: true
29 | })
30 |
31 | var hotMiddleware = require('webpack-hot-middleware')(compiler, {
32 | log: () => {}
33 | })
34 |
35 | // @TODO - fix this for webpack 3
36 | // force page reload when html-webpack-plugin template changes
37 | // compiler.plugin('compilation', function (compilation) {
38 | // compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
39 | // hotMiddleware.publish({ action: 'reload' })
40 | // cb()
41 | // })
42 | // })
43 |
44 | // proxy api requests
45 | Object.keys(proxyTable).forEach(function (context) {
46 | var options = proxyTable[context]
47 | if (typeof options === 'string') {
48 | options = { target: options }
49 | }
50 | app.use(proxyMiddleware(options.filter || context, options))
51 | })
52 |
53 | // handle fallback for HTML5 history API
54 | app.use(require('connect-history-api-fallback')())
55 |
56 | // serve webpack bundle output
57 | app.use(devMiddleware)
58 |
59 | // enable hot-reload and state-preserving
60 | // compilation error display
61 | app.use(hotMiddleware)
62 |
63 | // serve pure static assets
64 | var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
65 | app.use(staticPath, express.static('./static'))
66 |
67 | var uri = 'http://localhost:' + port
68 |
69 | devMiddleware.waitUntilValid(function () {
70 | console.log('> Listening at ' + uri + '\n')
71 | })
72 |
73 | module.exports = app.listen(port, function (err) {
74 | if (err) {
75 | console.log(err)
76 | return
77 | }
78 |
79 | // when env is testing, don't need open it
80 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
81 | opn(uri)
82 | }
83 | })
84 |
--------------------------------------------------------------------------------
/build/utils.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var config = require('../config')
3 | var ExtractTextPlugin = require('extract-text-webpack-plugin')
4 |
5 | exports.assetsPath = function (_path) {
6 | var assetsSubDirectory = process.env.NODE_ENV === 'production'
7 | ? config.build.assetsSubDirectory
8 | : config.dev.assetsSubDirectory
9 | return path.posix.join(assetsSubDirectory, _path)
10 | }
11 |
12 | exports.cssLoaders = function (options) {
13 | options = options || {}
14 | // generate loader string to be used with extract text plugin
15 | function generateLoaders (loaders) {
16 | var sourceLoader = loaders.map(function (loader) {
17 | var extraParamChar
18 | if (/\?/.test(loader)) {
19 | loader = loader.replace(/\?/, '-loader?')
20 | extraParamChar = '&'
21 | } else {
22 | loader = loader + '-loader'
23 | extraParamChar = '?'
24 | }
25 | return loader + (options.sourceMap ? extraParamChar + 'sourceMap' : '')
26 | }).join('!')
27 |
28 | // Extract CSS when that option is specified
29 | // (which is the case during production build)
30 | if (options.extract) {
31 | return ExtractTextPlugin.extract({
32 | use: sourceLoader,
33 | fallback: 'vue-style-loader'
34 | })
35 | } else {
36 | return ['vue-style-loader', sourceLoader].join('!')
37 | }
38 | }
39 |
40 | // http://vuejs.github.io/vue-loader/en/configurations/extract-css.html
41 | return {
42 | css: generateLoaders(['css']),
43 | postcss: generateLoaders(['css']),
44 | less: generateLoaders(['css', 'less']),
45 | sass: generateLoaders(['css', 'sass?indentedSyntax']),
46 | scss: generateLoaders(['css', 'sass']),
47 | stylus: generateLoaders(['css', 'stylus']),
48 | styl: generateLoaders(['css', 'stylus'])
49 | }
50 | }
51 |
52 | // Generate loaders for standalone style files (outside of .vue)
53 | exports.styleLoaders = function (options) {
54 | var output = []
55 | var loaders = exports.cssLoaders(options)
56 | for (var extension in loaders) {
57 | var loader = loaders[extension]
58 | output.push({
59 | test: new RegExp('\\.' + extension + '$'),
60 | loader: loader
61 | })
62 | }
63 | return output
64 | }
65 |
--------------------------------------------------------------------------------
/build/vue-loader.conf.js:
--------------------------------------------------------------------------------
1 | var utils = require('./utils')
2 | var config = require('../config')
3 | var isProduction = process.env.NODE_ENV === 'production'
4 |
5 | module.exports = {
6 | loaders: utils.cssLoaders({
7 | sourceMap: isProduction
8 | ? config.build.productionSourceMap
9 | : config.dev.cssSourceMap,
10 | extract: isProduction
11 | }),
12 | postcss: [
13 | require('autoprefixer')({
14 | browsers: ['last 2 versions']
15 | })
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/build/webpack.base.conf.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var utils = require('./utils')
3 | var config = require('../config')
4 | var vueLoaderConfig = require('./vue-loader.conf')
5 |
6 | function resolve (dir) {
7 | return path.join(__dirname, '..', dir)
8 | }
9 |
10 | module.exports = {
11 | entry: {
12 | app: './src/main.js'
13 | },
14 | output: {
15 | path: config.build.assetsRoot,
16 | filename: '[name].js',
17 | publicPath: process.env.NODE_ENV === 'production'
18 | ? config.build.assetsPublicPath
19 | : config.dev.assetsPublicPath
20 | },
21 | node: {
22 | Buffer: false,
23 | // prevent webpack from injecting useless setImmediate polyfill because Vue
24 | // source contains it (although only uses it if it's native).
25 | setImmediate: false,
26 | // prevent webpack from injecting mocks to Node native modules
27 | // that does not make sense for the client
28 | dgram: 'empty',
29 | fs: 'empty',
30 | net: 'empty',
31 | tls: 'empty',
32 | child_process: 'empty'
33 | },
34 | resolve: {
35 | extensions: ['.js', '.vue', '.json'],
36 | modules: [
37 | resolve('src'),
38 | resolve('node_modules')
39 | ],
40 | alias: {
41 | 'src': resolve('src'),
42 | 'assets': resolve('src/assets'),
43 | 'emoji': resolve('src/assets/emoji'),
44 | 'components': resolve('src/components'),
45 | 'pages': resolve('src/pages'),
46 | 'lib': resolve('src/lib'),
47 | 'Clappr': 'clappr'
48 | }
49 | },
50 | module: {
51 | rules: [
52 | // {
53 | // test: /\.(js|vue)$/,
54 | // loader: 'eslint-loader',
55 | // enforce: "pre",
56 | // include: [resolve('src'), resolve('test')],
57 | // options: {
58 | // formatter: require('eslint-friendly-formatter')
59 | // }
60 | // },
61 | {
62 | test: /\.vue$/,
63 | loader: 'vue-loader',
64 | options: vueLoaderConfig
65 | },
66 | {
67 | test: /\.js$/,
68 | loader: 'babel-loader',
69 | include: [resolve('src')]
70 | },
71 | {
72 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
73 | loader: 'file-loader',
74 | query: {
75 | name: utils.assetsPath('img/[name].[hash:7].[ext]')
76 | }
77 | },
78 | {
79 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
80 | loader: 'url-loader',
81 | query: {
82 | limit: 10000,
83 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
84 | }
85 | }
86 | ]
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/build/webpack.dev.conf.js:
--------------------------------------------------------------------------------
1 | var utils = require('./utils')
2 | var webpack = require('webpack')
3 | var config = require('../config')
4 | var merge = require('webpack-merge')
5 | var baseWebpackConfig = require('./webpack.base.conf')
6 | var HtmlWebpackPlugin = require('html-webpack-plugin')
7 | var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
8 |
9 | // add hot-reload related code to entry chunks
10 | Object.keys(baseWebpackConfig.entry).forEach(function (name) {
11 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
12 | })
13 |
14 | module.exports = merge(baseWebpackConfig, {
15 | module: {
16 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
17 | },
18 | // cheap-module-eval-source-map is faster for development
19 | devtool: '#cheap-module-eval-source-map',
20 | plugins: [
21 | new webpack.DefinePlugin({
22 | 'process.env': config.dev.env
23 | }),
24 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage
25 | new webpack.HotModuleReplacementPlugin(),
26 | new webpack.NoEmitOnErrorsPlugin(),
27 | // https://github.com/ampedandwired/html-webpack-plugin
28 | new HtmlWebpackPlugin({
29 | filename: 'index.html',
30 | template: 'index.html',
31 | inject: true
32 | }),
33 | new FriendlyErrorsPlugin()
34 | ]
35 | })
36 |
--------------------------------------------------------------------------------
/build/webpack.prod.conf.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const utils = require('./utils')
3 | const webpack = require('webpack')
4 | const config = require('../config')
5 | const merge = require('webpack-merge')
6 | const baseWebpackConfig = require('./webpack.base.conf')
7 | const HtmlWebpackPlugin = require('html-webpack-plugin')
8 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
9 | const OfflinePlugin = require('offline-plugin')
10 | const PurifyCSSPlugin = require('purifycss-webpack')
11 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
12 | const glob = require('glob')
13 | const env = config.build.env
14 |
15 | const webpackConfig = merge(baseWebpackConfig, {
16 | module: {
17 | rules: utils.styleLoaders({
18 | sourceMap: config.build.productionSourceMap,
19 | extract: true
20 | })
21 | },
22 | devtool: config.build.productionSourceMap ? '#source-map' : false,
23 | output: {
24 | path: config.build.assetsRoot,
25 | filename: utils.assetsPath('js/[name].[chunkhash].js'),
26 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
27 | },
28 | plugins: [
29 | // http://vuejs.github.io/vue-loader/en/workflow/production.html
30 | new webpack.DefinePlugin({
31 | 'process.env': env
32 | }),
33 | new UglifyJsPlugin({
34 | uglifyOptions: {
35 | compress: {
36 | warnings: false
37 | }
38 | },
39 | sourceMap: config.build.productionSourceMap,
40 | parallel: true
41 | }),
42 | // extract css into its own file
43 | new ExtractTextPlugin({
44 | filename: utils.assetsPath('css/[name].[contenthash].css')
45 | }),
46 | new PurifyCSSPlugin({
47 | paths: glob.sync(path.join(__dirname, '../src/**/*.vue')),
48 | minimize: true,
49 | purifyOptions: {
50 | whitelist: ['*data-v-*', 'sans-serif', '*tooltip*']
51 | }
52 | }),
53 | // generate dist index.html with correct asset hash for caching.
54 | // you can customize output by editing /index.html
55 | // see https://github.com/ampedandwired/html-webpack-plugin
56 | new HtmlWebpackPlugin({
57 | filename: config.build.index,
58 | template: 'index.html',
59 | inject: true,
60 | minify: {
61 | removeComments: true,
62 | collapseWhitespace: true,
63 | removeAttributeQuotes: true
64 | // more options:
65 | // https://github.com/kangax/html-minifier#options-quick-reference
66 | },
67 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin
68 | chunksSortMode: 'dependency'
69 | }),
70 | // enable scope hoisting
71 | new webpack.optimize.ModuleConcatenationPlugin(),
72 | // split vendor js into its own file
73 | new webpack.optimize.CommonsChunkPlugin({
74 | name: 'vendor',
75 | minChunks: function (module, count) {
76 | // any required modules inside node_modules are extracted to vendor
77 | return (
78 | module.resource &&
79 | /\.js$/.test(module.resource) &&
80 | module.resource.indexOf(
81 | path.join(__dirname, '../node_modules')
82 | ) === 0
83 | )
84 | }
85 | }),
86 | // extract webpack runtime and module manifest to its own file in order to
87 | // prevent vendor hash from being updated whenever app bundle is updated
88 | new webpack.optimize.CommonsChunkPlugin({
89 | name: 'manifest',
90 | chunks: ['vendor']
91 | }),
92 | new webpack.LoaderOptionsPlugin({
93 | minimize: true
94 | }),
95 | new OfflinePlugin({
96 | ServiceWorker: {
97 | events: true,
98 | navigateFallbackURL: '/'
99 | },
100 | AppCache: false,
101 | cacheMaps: [{
102 | match: function (url) {
103 | if (url.pathname.indexOf('/cdn/') > -1) {
104 | return
105 | }
106 |
107 | return new URL('/', location)
108 | },
109 | requestTypes: ['navigate']
110 | }]
111 | })
112 | ]
113 | })
114 |
115 | if (config.build.productionGzip) {
116 | const CompressionWebpackPlugin = require('compression-webpack-plugin')
117 |
118 | webpackConfig.plugins.push(
119 | new CompressionWebpackPlugin({
120 | asset: '[path].gz[query]',
121 | algorithm: 'gzip',
122 | test: new RegExp(
123 | '\\.(' +
124 | config.build.productionGzipExtensions.join('|') +
125 | ')$'
126 | ),
127 | threshold: 10240,
128 | minRatio: 0.8
129 | })
130 | )
131 | }
132 |
133 | if (config.build.bundleAnalyzerReport) {
134 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
135 | webpackConfig.plugins.push(new BundleAnalyzerPlugin())
136 | }
137 |
138 | module.exports = webpackConfig
139 |
--------------------------------------------------------------------------------
/config/dev.env.js:
--------------------------------------------------------------------------------
1 | var merge = require('webpack-merge')
2 | var prodEnv = require('./prod.env')
3 |
4 | module.exports = merge(prodEnv, {
5 | NODE_ENV: '"development"'
6 | })
7 |
--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
1 | // see http://vuejs-templates.github.io/webpack for documentation.
2 | var path = require('path')
3 |
4 | module.exports = {
5 | build: {
6 | env: require('./prod.env'),
7 | index: path.resolve(__dirname, '../dist/index.html'),
8 | assetsRoot: path.resolve(__dirname, '../dist'),
9 | assetsSubDirectory: 'static',
10 | assetsPublicPath: '/',
11 | productionSourceMap: false,
12 | // Gzip off by default as many popular static hosts such as
13 | // Surge or Netlify already gzip all static assets for you.
14 | // Before setting to `true`, make sure to:
15 | // npm install --save-dev compression-webpack-plugin
16 | productionGzip: false,
17 | productionGzipExtensions: ['js', 'css'],
18 | // Run the build command with an extra argument to
19 | // View the bundle analyzer report after build finishes:
20 | // `npm run build --report`
21 | // Set to `true` or `false` to always turn it on or off
22 | bundleAnalyzerReport: process.env.npm_config_report
23 | },
24 | dev: {
25 | env: require('./dev.env'),
26 | port: 8080,
27 | autoOpenBrowser: true,
28 | assetsSubDirectory: 'static',
29 | assetsPublicPath: '/',
30 | proxyTable: {
31 | '/cdn': {
32 | target: 'http://img1.ak.crunchyroll.com',
33 | changeOrigin: true,
34 | pathRewrite: {
35 | '^/cdn': ''
36 | }
37 | },
38 | '/pl-proxy': {
39 | target: 'https://pl.crunchyroll.com',
40 | changeOrigin: true,
41 | pathRewrite: {
42 | '^/pl-proxy': ''
43 | }
44 | }
45 | },
46 | // CSS Sourcemaps off by default because relative paths are "buggy"
47 | // with this option, according to the CSS-Loader README
48 | // (https://github.com/webpack/css-loader#sourcemaps)
49 | // In our experience, they generally work as expected,
50 | // just be aware of this issue when enabling this option.
51 | cssSourceMap: false
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/config/prod.env.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | NODE_ENV: '"production"'
3 | }
4 |
--------------------------------------------------------------------------------
/extension/background.js:
--------------------------------------------------------------------------------
1 | /* global chrome */
2 |
3 | chrome.webRequest.onHeadersReceived.addListener(({responseHeaders}) => {
4 | const corsHeader = responseHeaders.findIndex(({name}) => name.toLowerCase() === 'access-control-allow-origin')
5 | if (corsHeader > -1) {
6 | responseHeaders[corsHeader].value = '*'
7 | }
8 |
9 | return {responseHeaders}
10 | }, {urls: ['https://*.vrv.co/*', 'https://*.dlvr1.net/*']}, ['blocking', 'responseHeaders'])
11 |
--------------------------------------------------------------------------------
/extension/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 |
4 | "name": "Umi Enabler",
5 | "description": "Allows Umi player to work on all series",
6 | "version": "1.0.0",
7 |
8 | "permissions": [
9 | "webRequest",
10 | "webRequestBlocking",
11 | "https://umi.party/*",
12 | "https://*.vrv.co/*",
13 | "https://*.dlvr1.net/*"
14 | ],
15 |
16 | "background": {
17 | "scripts": ["background.js"],
18 | "persistent": true
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/functions/index.js:
--------------------------------------------------------------------------------
1 | const functions = require('firebase-functions')
2 | const admin = require('firebase-admin')
3 | admin.initializeApp(functions.config().firebase)
4 |
5 | exports.cleanRooms = functions.database.ref('roomUsers/{roomId}')
6 | .onDelete((event) => {
7 | const root = event.data.adminRef.root
8 | return Promise.all([
9 | root.child(`rooms/${event.params.roomId}`).remove(),
10 | root.child(`roomEmoji/${event.params.roomId}`).remove()
11 | ])
12 | })
13 |
--------------------------------------------------------------------------------
/functions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "functions",
3 | "description": "Cloud Functions for Firebase",
4 | "scripts": {
5 | "serve": "firebase serve --only functions",
6 | "shell": "firebase experimental:functions:shell",
7 | "start": "npm run shell",
8 | "deploy": "firebase deploy --only functions",
9 | "logs": "firebase functions:log"
10 | },
11 | "dependencies": {
12 | "firebase-admin": "~5.4.2",
13 | "firebase-functions": "^0.7.1"
14 | },
15 | "private": true
16 | }
17 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Watch anime together - Umi
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "umi",
3 | "version": "1.0.0",
4 | "description": "Better Crunchyroll",
5 | "author": "Zach Bruggeman ",
6 | "private": true,
7 | "scripts": {
8 | "dev": "node build/dev-server.js",
9 | "build": "node build/build.js && cp _redirects dist/_redirects",
10 | "lint": "eslint --ext .js,.vue src"
11 | },
12 | "dependencies": {
13 | "animejs": "^2.0.1",
14 | "axios": "^0.16.0",
15 | "base64-js": "^1.2.0",
16 | "clappr": "0.2.77",
17 | "clappr-thumbnails-plugin": "^3.6.0",
18 | "date-fns": "^1.27.2",
19 | "debounce": "^1.0.2",
20 | "firebase": "^4.6.2",
21 | "font-awesome": "^4.7.0",
22 | "tachyons": "^4.9.0",
23 | "uuid": "^3.0.1",
24 | "v-tooltip": "^1.1.6",
25 | "vue": "^2.2.4",
26 | "vue-analytics": "^4.1.3",
27 | "vue-clickaway": "^2.1.0",
28 | "vue-meta": "^1.0.4",
29 | "vue-router": "^2.3.0",
30 | "vuex": "^2.2.1",
31 | "vuex-router-sync": "^4.1.2"
32 | },
33 | "devDependencies": {
34 | "autoprefixer": "^7.2.5",
35 | "babel-core": "^6.26.0",
36 | "babel-eslint": "^7.1.1",
37 | "babel-loader": "^7.0.0",
38 | "babel-polyfill": "^6.23.0",
39 | "babel-preset-env": "^1.6.1",
40 | "babel-register": "^6.22.0",
41 | "chalk": "^1.1.3",
42 | "connect-history-api-fallback": "^1.3.0",
43 | "css-loader": "^0.28.0",
44 | "eslint": "^3.14.1",
45 | "eslint-config-standard": "^10.0.0",
46 | "eslint-friendly-formatter": "^2.0.7",
47 | "eslint-loader": "^1.6.1",
48 | "eslint-plugin-import": "^2.2.0",
49 | "eslint-plugin-node": "^4.2.2",
50 | "eslint-plugin-promise": "^3.4.0",
51 | "eslint-plugin-standard": "^3.0.1",
52 | "eslint-plugin-vue": "^4.0.1",
53 | "eventsource-polyfill": "^0.9.6",
54 | "express": "^4.14.1",
55 | "extract-text-webpack-plugin": "^3.0.0",
56 | "file-loader": "^0.11.1",
57 | "friendly-errors-webpack-plugin": "^1.1.3",
58 | "function-bind": "^1.1.0",
59 | "html-webpack-plugin": "^2.28.0",
60 | "http-proxy-middleware": "^0.17.3",
61 | "node": "^8.9.4",
62 | "offline-plugin": "^4.6.2",
63 | "opn": "^5.0.0",
64 | "ora": "^1.1.0",
65 | "purify-css": "^1.2.5",
66 | "purifycss-webpack": "^0.7.0",
67 | "semver": "^5.3.0",
68 | "shelljs": "^0.7.6",
69 | "uglifyjs-webpack-plugin": "^1.1.2",
70 | "url-loader": "^0.5.7",
71 | "vue-loader": "^13.0.4",
72 | "vue-style-loader": "^3.0.1",
73 | "vue-template-compiler": "^2.2.4",
74 | "webpack": "^3.3.0",
75 | "webpack-bundle-analyzer": "^2.2.1",
76 | "webpack-dev-middleware": "^1.10.0",
77 | "webpack-hot-middleware": "^2.16.1",
78 | "webpack-merge": "^4.0.0",
79 | "workerize-loader": "^1.0.1"
80 | },
81 | "engines": {
82 | "node": ">= 4.0.0",
83 | "npm": ">= 3.0.0"
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const compression = require('compression')
3 | const cors = require('cors')
4 |
5 | const opening = require('./routes/opening')
6 | const mal = require('./routes/mal')
7 |
8 | // create app
9 | const app = express()
10 | const srv = require('http').Server(app)
11 | app.use(compression())
12 | app.use(cors())
13 |
14 | // setup routes
15 | app.get('/', (req, res) => res.send({status: 'ok'}))
16 | app.get('/opening', opening)
17 | app.use('/mal', mal)
18 |
19 | // listen
20 | srv.listen(3001, () => {
21 | console.log('listening on 3001')
22 | })
23 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "umi watch together server",
5 | "main": "index.js",
6 | "author": "Zach Bruggeman",
7 | "license": "MIT",
8 | "dependencies": {
9 | "axios": "^0.16.2",
10 | "body-parser": "^1.17.1",
11 | "cheerio": "^0.22.0",
12 | "compression": "^1.6.2",
13 | "cors": "^2.8.3",
14 | "express": "^4.15.2",
15 | "iron": "^4.0.4",
16 | "popura": "^1.2.5"
17 | },
18 | "scripts": {
19 | "start": "node index.js",
20 | "dev": "cross-env IRON_TOKEN=EXAMPLE_TOKEN_PLEASE_DONT_USE_IN_PRODUCTION nodemon index.js"
21 | },
22 | "now": {
23 | "alias": "umi-watch-api",
24 | "env": {
25 | "IRON_TOKEN": "@iron-token",
26 | "NODE_ENV": "production"
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/server/routes/mal.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const axios = require('axios')
3 | const {json} = require('body-parser')
4 | const popura = require('popura')
5 | const Iron = require('iron')
6 |
7 | const {IRON_TOKEN} = process.env
8 |
9 | const router = express.Router()
10 | router.use(json())
11 |
12 | router.post('/login', function malLogin (req, res) {
13 | const {username, password} = req.body
14 | if (!username || !password) return res.status(400).send({status: 'not ok', error: 'Invalid login'})
15 |
16 | const client = popura(username, password)
17 |
18 | client.verifyAuth()
19 | .then((obj) => {
20 | Iron.seal(password, IRON_TOKEN, Iron.defaults, (err, sealed) => {
21 | if (err) return res.status(500).send({status: 'not ok', error: 'Internal error'})
22 |
23 | res.send({
24 | status: 'ok',
25 | username: obj.username,
26 | password: sealed
27 | })
28 | })
29 | })
30 | .catch((err) => {
31 | res.status(err.statusCode).send({
32 | status: 'not ok',
33 | error: err.message
34 | })
35 | })
36 | })
37 |
38 | router.get('/series', function malSeries (req, res) {
39 | const {name} = req.query
40 | if (!name) return res.status(400).send({status: 'not ok', error: 'Invalid payload'})
41 |
42 | axios({
43 | method: 'GET',
44 | url: 'https://myanimelist.net/search/prefix.json',
45 | params: {
46 | type: 'anime',
47 | keyword: name,
48 | v: 1
49 | }
50 | })
51 | .then(({data}) => {
52 | const {items} = data.categories[0]
53 |
54 | let item = items.find((i) => i.name.toLowerCase() === name.toLowerCase()) || items[0]
55 | if (name === 'My Hero Academia Season 2') {
56 | item = items.find((i) => i.name === 'Boku no Hero Academia 2nd Season') || item
57 | }
58 | if (!item) return res.status(404).send({status: 'not ok', error: `Couldn't find anime for "${name}"`})
59 |
60 | res.send({status: 'ok', item})
61 | })
62 | })
63 |
64 | router.post('/update', function malUpdate (req, res) {
65 | const {username, password, id, episode, status} = req.body
66 | if (!username || !password) return res.status(400).send({status: 'not ok', error: 'Invalid login'})
67 | if (!id || !episode || !status) return res.status(400).send({status: 'not ok', error: 'Invalid payload'})
68 |
69 | Iron.unseal(password, IRON_TOKEN, Iron.defaults, (err, unsealed) => {
70 | if (err) return res.status(500).send({status: 'not ok', error: 'Internal error'})
71 |
72 | const client = popura(username, unsealed)
73 | client.updateAnime(id, {episode, status})
74 | .then(() => {
75 | res.send({status: 'ok'})
76 | })
77 | .catch((err) => {
78 | console.error(err)
79 | res.status(500).send({status: 'not ok', error: 'Internal error'})
80 | })
81 | })
82 | })
83 |
84 | module.exports = router
85 |
--------------------------------------------------------------------------------
/server/routes/opening.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios')
2 | const cheerio = require('cheerio')
3 | const cache = {}
4 |
5 | async function openingHandler (req, res) {
6 | const {search} = req.query
7 | if (!search) return res.send({result: null})
8 | if (cache[search]) {
9 | const openings = cache[search]
10 | const result = openings[Math.floor(Math.random() * openings.length)].url
11 | return res.send({result})
12 | }
13 |
14 | try {
15 | const {data: results} = await axios.get(`https://openings.ninja/core/getAnime.php?term=${search}`, {timeout: 3000})
16 | if (!results || results.length === 0) return res.send({result: null})
17 |
18 | const {data: html} = await axios.get(`https://openings.ninja/${results[0]}/op/1`)
19 | const $ = cheerio.load(html)
20 | const openings = []
21 | $('#mirror > li[data-theme^="op"]').toArray()
22 | .forEach((el) => {
23 | const index = openings.findIndex((op) => op.theme === el.attribs['data-theme'])
24 | if (index < 0) {
25 | openings.push({theme: el.attribs['data-theme'], url: el.attribs['data-url']})
26 | }
27 | })
28 |
29 | cache[search] = openings
30 | const result = openings[Math.floor(Math.random() * openings.length)].url
31 | res.send({result})
32 | } catch (err) {
33 | console.error(err.message)
34 | res.send({result: null})
35 | }
36 | }
37 |
38 | module.exports = openingHandler
39 |
--------------------------------------------------------------------------------
/server/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | accepts@~1.3.3:
6 | version "1.3.3"
7 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca"
8 | dependencies:
9 | mime-types "~2.1.11"
10 | negotiator "0.6.1"
11 |
12 | array-flatten@1.1.1:
13 | version "1.1.1"
14 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
15 |
16 | axios@^0.16.2:
17 | version "0.16.2"
18 | resolved "https://registry.yarnpkg.com/axios/-/axios-0.16.2.tgz#ba4f92f17167dfbab40983785454b9ac149c3c6d"
19 | dependencies:
20 | follow-redirects "^1.2.3"
21 | is-buffer "^1.1.5"
22 |
23 | body-parser@^1.17.1:
24 | version "1.17.2"
25 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.2.tgz#f8892abc8f9e627d42aedafbca66bf5ab99104ee"
26 | dependencies:
27 | bytes "2.4.0"
28 | content-type "~1.0.2"
29 | debug "2.6.7"
30 | depd "~1.1.0"
31 | http-errors "~1.6.1"
32 | iconv-lite "0.4.15"
33 | on-finished "~2.3.0"
34 | qs "6.4.0"
35 | raw-body "~2.2.0"
36 | type-is "~1.6.15"
37 |
38 | boolbase@~1.0.0:
39 | version "1.0.0"
40 | resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
41 |
42 | boom@5.x.x:
43 | version "5.1.0"
44 | resolved "https://registry.yarnpkg.com/boom/-/boom-5.1.0.tgz#0308fa8e924cd6d42d9c3bf4883bdc98f0e71df8"
45 | dependencies:
46 | hoek "4.x.x"
47 |
48 | bytes@2.3.0:
49 | version "2.3.0"
50 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.3.0.tgz#d5b680a165b6201739acb611542aabc2d8ceb070"
51 |
52 | bytes@2.4.0:
53 | version "2.4.0"
54 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339"
55 |
56 | capture-stack-trace@^1.0.0:
57 | version "1.0.0"
58 | resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d"
59 |
60 | cheerio@^0.22.0:
61 | version "0.22.0"
62 | resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.22.0.tgz#a9baa860a3f9b595a6b81b1a86873121ed3a269e"
63 | dependencies:
64 | css-select "~1.2.0"
65 | dom-serializer "~0.1.0"
66 | entities "~1.1.1"
67 | htmlparser2 "^3.9.1"
68 | lodash.assignin "^4.0.9"
69 | lodash.bind "^4.1.4"
70 | lodash.defaults "^4.0.1"
71 | lodash.filter "^4.4.0"
72 | lodash.flatten "^4.2.0"
73 | lodash.foreach "^4.3.0"
74 | lodash.map "^4.4.0"
75 | lodash.merge "^4.4.0"
76 | lodash.pick "^4.2.1"
77 | lodash.reduce "^4.4.0"
78 | lodash.reject "^4.4.0"
79 | lodash.some "^4.4.0"
80 |
81 | compressible@~2.0.8:
82 | version "2.0.10"
83 | resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.10.tgz#feda1c7f7617912732b29bf8cf26252a20b9eecd"
84 | dependencies:
85 | mime-db ">= 1.27.0 < 2"
86 |
87 | compression@^1.6.2:
88 | version "1.6.2"
89 | resolved "https://registry.yarnpkg.com/compression/-/compression-1.6.2.tgz#cceb121ecc9d09c52d7ad0c3350ea93ddd402bc3"
90 | dependencies:
91 | accepts "~1.3.3"
92 | bytes "2.3.0"
93 | compressible "~2.0.8"
94 | debug "~2.2.0"
95 | on-headers "~1.0.1"
96 | vary "~1.1.0"
97 |
98 | content-disposition@0.5.2:
99 | version "0.5.2"
100 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
101 |
102 | content-type@~1.0.2:
103 | version "1.0.2"
104 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed"
105 |
106 | cookie-signature@1.0.6:
107 | version "1.0.6"
108 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
109 |
110 | cookie@0.3.1:
111 | version "0.3.1"
112 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
113 |
114 | core-util-is@~1.0.0:
115 | version "1.0.2"
116 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
117 |
118 | cors@^2.8.3:
119 | version "2.8.3"
120 | resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.3.tgz#4cf78e1d23329a7496b2fc2225b77ca5bb5eb802"
121 | dependencies:
122 | object-assign "^4"
123 | vary "^1"
124 |
125 | create-error-class@^3.0.0:
126 | version "3.0.2"
127 | resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6"
128 | dependencies:
129 | capture-stack-trace "^1.0.0"
130 |
131 | cryptiles@3.x.x:
132 | version "3.1.2"
133 | resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe"
134 | dependencies:
135 | boom "5.x.x"
136 |
137 | css-select@~1.2.0:
138 | version "1.2.0"
139 | resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
140 | dependencies:
141 | boolbase "~1.0.0"
142 | css-what "2.1"
143 | domutils "1.5.1"
144 | nth-check "~1.0.1"
145 |
146 | css-what@2.1:
147 | version "2.1.0"
148 | resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd"
149 |
150 | debug@2.6.7, debug@^2.2.0, debug@^2.4.5:
151 | version "2.6.7"
152 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e"
153 | dependencies:
154 | ms "2.0.0"
155 |
156 | debug@~2.2.0:
157 | version "2.2.0"
158 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da"
159 | dependencies:
160 | ms "0.7.1"
161 |
162 | depd@1.1.0, depd@~1.1.0:
163 | version "1.1.0"
164 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3"
165 |
166 | destroy@~1.0.4:
167 | version "1.0.4"
168 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
169 |
170 | dom-serializer@0, dom-serializer@~0.1.0:
171 | version "0.1.0"
172 | resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
173 | dependencies:
174 | domelementtype "~1.1.1"
175 | entities "~1.1.1"
176 |
177 | domelementtype@1, domelementtype@^1.3.0:
178 | version "1.3.0"
179 | resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2"
180 |
181 | domelementtype@~1.1.1:
182 | version "1.1.3"
183 | resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b"
184 |
185 | domhandler@^2.3.0:
186 | version "2.4.1"
187 | resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.1.tgz#892e47000a99be55bbf3774ffea0561d8879c259"
188 | dependencies:
189 | domelementtype "1"
190 |
191 | domutils@1.5.1, domutils@^1.5.1:
192 | version "1.5.1"
193 | resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
194 | dependencies:
195 | dom-serializer "0"
196 | domelementtype "1"
197 |
198 | duplexer3@^0.1.4:
199 | version "0.1.4"
200 | resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
201 |
202 | ee-first@1.1.1:
203 | version "1.1.1"
204 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
205 |
206 | encodeurl@~1.0.1:
207 | version "1.0.1"
208 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20"
209 |
210 | entities@^1.1.1, entities@~1.1.1:
211 | version "1.1.1"
212 | resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
213 |
214 | escape-html@~1.0.3:
215 | version "1.0.3"
216 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
217 |
218 | etag@~1.8.0:
219 | version "1.8.0"
220 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051"
221 |
222 | express@^4.15.2:
223 | version "4.15.3"
224 | resolved "https://registry.yarnpkg.com/express/-/express-4.15.3.tgz#bab65d0f03aa80c358408972fc700f916944b662"
225 | dependencies:
226 | accepts "~1.3.3"
227 | array-flatten "1.1.1"
228 | content-disposition "0.5.2"
229 | content-type "~1.0.2"
230 | cookie "0.3.1"
231 | cookie-signature "1.0.6"
232 | debug "2.6.7"
233 | depd "~1.1.0"
234 | encodeurl "~1.0.1"
235 | escape-html "~1.0.3"
236 | etag "~1.8.0"
237 | finalhandler "~1.0.3"
238 | fresh "0.5.0"
239 | merge-descriptors "1.0.1"
240 | methods "~1.1.2"
241 | on-finished "~2.3.0"
242 | parseurl "~1.3.1"
243 | path-to-regexp "0.1.7"
244 | proxy-addr "~1.1.4"
245 | qs "6.4.0"
246 | range-parser "~1.2.0"
247 | send "0.15.3"
248 | serve-static "1.12.3"
249 | setprototypeof "1.0.3"
250 | statuses "~1.3.1"
251 | type-is "~1.6.15"
252 | utils-merge "1.0.0"
253 | vary "~1.1.1"
254 |
255 | finalhandler@~1.0.3:
256 | version "1.0.3"
257 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.3.tgz#ef47e77950e999780e86022a560e3217e0d0cc89"
258 | dependencies:
259 | debug "2.6.7"
260 | encodeurl "~1.0.1"
261 | escape-html "~1.0.3"
262 | on-finished "~2.3.0"
263 | parseurl "~1.3.1"
264 | statuses "~1.3.1"
265 | unpipe "~1.0.0"
266 |
267 | follow-redirects@^1.2.3:
268 | version "1.2.4"
269 | resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.2.4.tgz#355e8f4d16876b43f577b0d5ce2668b9723214ea"
270 | dependencies:
271 | debug "^2.4.5"
272 |
273 | forwarded@~0.1.0:
274 | version "0.1.0"
275 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363"
276 |
277 | fresh@0.5.0:
278 | version "0.5.0"
279 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e"
280 |
281 | get-stream@^3.0.0:
282 | version "3.0.0"
283 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
284 |
285 | got@^6.3.0:
286 | version "6.7.1"
287 | resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0"
288 | dependencies:
289 | create-error-class "^3.0.0"
290 | duplexer3 "^0.1.4"
291 | get-stream "^3.0.0"
292 | is-redirect "^1.0.0"
293 | is-retry-allowed "^1.0.0"
294 | is-stream "^1.0.0"
295 | lowercase-keys "^1.0.0"
296 | safe-buffer "^5.0.1"
297 | timed-out "^4.0.0"
298 | unzip-response "^2.0.1"
299 | url-parse-lax "^1.0.0"
300 |
301 | hoek@4.x.x:
302 | version "4.1.1"
303 | resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.1.1.tgz#9cc573ffba2b7b408fb5e9c2a13796be94cddce9"
304 |
305 | htmlparser2@^3.9.1:
306 | version "3.9.2"
307 | resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
308 | dependencies:
309 | domelementtype "^1.3.0"
310 | domhandler "^2.3.0"
311 | domutils "^1.5.1"
312 | entities "^1.1.1"
313 | inherits "^2.0.1"
314 | readable-stream "^2.0.2"
315 |
316 | http-errors@~1.6.1:
317 | version "1.6.1"
318 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257"
319 | dependencies:
320 | depd "1.1.0"
321 | inherits "2.0.3"
322 | setprototypeof "1.0.3"
323 | statuses ">= 1.3.1 < 2"
324 |
325 | iconv-lite@0.4.15:
326 | version "0.4.15"
327 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb"
328 |
329 | inherits@2.0.3, inherits@^2.0.1, inherits@~2.0.3:
330 | version "2.0.3"
331 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
332 |
333 | ipaddr.js@1.3.0:
334 | version "1.3.0"
335 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.3.0.tgz#1e03a52fdad83a8bbb2b25cbf4998b4cffcd3dec"
336 |
337 | iron@^4.0.4:
338 | version "4.0.5"
339 | resolved "https://registry.yarnpkg.com/iron/-/iron-4.0.5.tgz#4f042cceb8b9738f346b59aa734c83a89bc31428"
340 | dependencies:
341 | boom "5.x.x"
342 | cryptiles "3.x.x"
343 | hoek "4.x.x"
344 |
345 | is-buffer@^1.1.5:
346 | version "1.1.5"
347 | resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc"
348 |
349 | is-redirect@^1.0.0:
350 | version "1.0.0"
351 | resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24"
352 |
353 | is-retry-allowed@^1.0.0:
354 | version "1.1.0"
355 | resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34"
356 |
357 | is-stream@^1.0.0:
358 | version "1.1.0"
359 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
360 |
361 | isarray@~1.0.0:
362 | version "1.0.0"
363 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
364 |
365 | lodash.assignin@^4.0.9:
366 | version "4.2.0"
367 | resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2"
368 |
369 | lodash.bind@^4.1.4:
370 | version "4.2.1"
371 | resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35"
372 |
373 | lodash.defaults@^4.0.1:
374 | version "4.2.0"
375 | resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
376 |
377 | lodash.filter@^4.4.0:
378 | version "4.6.0"
379 | resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace"
380 |
381 | lodash.flatten@^4.2.0:
382 | version "4.4.0"
383 | resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
384 |
385 | lodash.foreach@^4.3.0:
386 | version "4.5.0"
387 | resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53"
388 |
389 | lodash.map@^4.4.0:
390 | version "4.6.0"
391 | resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3"
392 |
393 | lodash.merge@^4.4.0:
394 | version "4.6.0"
395 | resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5"
396 |
397 | lodash.pick@^4.2.1:
398 | version "4.4.0"
399 | resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
400 |
401 | lodash.reduce@^4.4.0:
402 | version "4.6.0"
403 | resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b"
404 |
405 | lodash.reject@^4.4.0:
406 | version "4.6.0"
407 | resolved "https://registry.yarnpkg.com/lodash.reject/-/lodash.reject-4.6.0.tgz#80d6492dc1470864bbf583533b651f42a9f52415"
408 |
409 | lodash.some@^4.4.0:
410 | version "4.6.0"
411 | resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d"
412 |
413 | lodash@^4.0.0:
414 | version "4.17.4"
415 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
416 |
417 | lowercase-keys@^1.0.0:
418 | version "1.0.0"
419 | resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306"
420 |
421 | media-typer@0.3.0:
422 | version "0.3.0"
423 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
424 |
425 | merge-descriptors@1.0.1:
426 | version "1.0.1"
427 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
428 |
429 | methods@~1.1.2:
430 | version "1.1.2"
431 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
432 |
433 | "mime-db@>= 1.27.0 < 2":
434 | version "1.28.0"
435 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.28.0.tgz#fedd349be06d2865b7fc57d837c6de4f17d7ac3c"
436 |
437 | mime-db@~1.27.0:
438 | version "1.27.0"
439 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1"
440 |
441 | mime-types@~2.1.11, mime-types@~2.1.15:
442 | version "2.1.15"
443 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed"
444 | dependencies:
445 | mime-db "~1.27.0"
446 |
447 | mime@1.3.4:
448 | version "1.3.4"
449 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53"
450 |
451 | ms@0.7.1:
452 | version "0.7.1"
453 | resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098"
454 |
455 | ms@2.0.0:
456 | version "2.0.0"
457 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
458 |
459 | negotiator@0.6.1:
460 | version "0.6.1"
461 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
462 |
463 | nth-check@~1.0.1:
464 | version "1.0.1"
465 | resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.1.tgz#9929acdf628fc2c41098deab82ac580cf149aae4"
466 | dependencies:
467 | boolbase "~1.0.0"
468 |
469 | object-assign@^4:
470 | version "4.1.1"
471 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
472 |
473 | on-finished@~2.3.0:
474 | version "2.3.0"
475 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
476 | dependencies:
477 | ee-first "1.1.1"
478 |
479 | on-headers@~1.0.1:
480 | version "1.0.1"
481 | resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7"
482 |
483 | parseurl@~1.3.1:
484 | version "1.3.1"
485 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56"
486 |
487 | path-to-regexp@0.1.7:
488 | version "0.1.7"
489 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
490 |
491 | popura@^1.2.5:
492 | version "1.2.5"
493 | resolved "https://registry.yarnpkg.com/popura/-/popura-1.2.5.tgz#a92cd25fb940c2d6a5854029b825798a95c1901a"
494 | dependencies:
495 | debug "^2.2.0"
496 | got "^6.3.0"
497 | xml2js "^0.4.17"
498 |
499 | prepend-http@^1.0.1:
500 | version "1.0.4"
501 | resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
502 |
503 | process-nextick-args@~1.0.6:
504 | version "1.0.7"
505 | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
506 |
507 | proxy-addr@~1.1.4:
508 | version "1.1.4"
509 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.4.tgz#27e545f6960a44a627d9b44467e35c1b6b4ce2f3"
510 | dependencies:
511 | forwarded "~0.1.0"
512 | ipaddr.js "1.3.0"
513 |
514 | qs@6.4.0:
515 | version "6.4.0"
516 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
517 |
518 | range-parser@~1.2.0:
519 | version "1.2.0"
520 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
521 |
522 | raw-body@~2.2.0:
523 | version "2.2.0"
524 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.2.0.tgz#994976cf6a5096a41162840492f0bdc5d6e7fb96"
525 | dependencies:
526 | bytes "2.4.0"
527 | iconv-lite "0.4.15"
528 | unpipe "1.0.0"
529 |
530 | readable-stream@^2.0.2:
531 | version "2.3.2"
532 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.2.tgz#5a04df05e4f57fe3f0dc68fdd11dc5c97c7e6f4d"
533 | dependencies:
534 | core-util-is "~1.0.0"
535 | inherits "~2.0.3"
536 | isarray "~1.0.0"
537 | process-nextick-args "~1.0.6"
538 | safe-buffer "~5.1.0"
539 | string_decoder "~1.0.0"
540 | util-deprecate "~1.0.1"
541 |
542 | safe-buffer@^5.0.1, safe-buffer@~5.1.0:
543 | version "5.1.1"
544 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
545 |
546 | sax@>=0.6.0:
547 | version "1.2.4"
548 | resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
549 |
550 | send@0.15.3:
551 | version "0.15.3"
552 | resolved "https://registry.yarnpkg.com/send/-/send-0.15.3.tgz#5013f9f99023df50d1bd9892c19e3defd1d53309"
553 | dependencies:
554 | debug "2.6.7"
555 | depd "~1.1.0"
556 | destroy "~1.0.4"
557 | encodeurl "~1.0.1"
558 | escape-html "~1.0.3"
559 | etag "~1.8.0"
560 | fresh "0.5.0"
561 | http-errors "~1.6.1"
562 | mime "1.3.4"
563 | ms "2.0.0"
564 | on-finished "~2.3.0"
565 | range-parser "~1.2.0"
566 | statuses "~1.3.1"
567 |
568 | serve-static@1.12.3:
569 | version "1.12.3"
570 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.3.tgz#9f4ba19e2f3030c547f8af99107838ec38d5b1e2"
571 | dependencies:
572 | encodeurl "~1.0.1"
573 | escape-html "~1.0.3"
574 | parseurl "~1.3.1"
575 | send "0.15.3"
576 |
577 | setprototypeof@1.0.3:
578 | version "1.0.3"
579 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04"
580 |
581 | "statuses@>= 1.3.1 < 2", statuses@~1.3.1:
582 | version "1.3.1"
583 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
584 |
585 | string_decoder@~1.0.0:
586 | version "1.0.3"
587 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
588 | dependencies:
589 | safe-buffer "~5.1.0"
590 |
591 | timed-out@^4.0.0:
592 | version "4.0.1"
593 | resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"
594 |
595 | type-is@~1.6.15:
596 | version "1.6.15"
597 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410"
598 | dependencies:
599 | media-typer "0.3.0"
600 | mime-types "~2.1.15"
601 |
602 | unpipe@1.0.0, unpipe@~1.0.0:
603 | version "1.0.0"
604 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
605 |
606 | unzip-response@^2.0.1:
607 | version "2.0.1"
608 | resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97"
609 |
610 | url-parse-lax@^1.0.0:
611 | version "1.0.0"
612 | resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73"
613 | dependencies:
614 | prepend-http "^1.0.1"
615 |
616 | util-deprecate@~1.0.1:
617 | version "1.0.2"
618 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
619 |
620 | utils-merge@1.0.0:
621 | version "1.0.0"
622 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8"
623 |
624 | vary@^1, vary@~1.1.0, vary@~1.1.1:
625 | version "1.1.1"
626 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37"
627 |
628 | xml2js@^0.4.17:
629 | version "0.4.17"
630 | resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.17.tgz#17be93eaae3f3b779359c795b419705a8817e868"
631 | dependencies:
632 | sax ">=0.6.0"
633 | xmlbuilder "^4.1.0"
634 |
635 | xmlbuilder@^4.1.0:
636 | version "4.2.1"
637 | resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-4.2.1.tgz#aa58a3041a066f90eaa16c2f5389ff19f3f461a5"
638 | dependencies:
639 | lodash "^4.0.0"
640 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | @import '~tachyons';
2 | @import '~font-awesome/css/font-awesome.css';
3 |
4 | @keyframes loading-shimmer {
5 | 0% {
6 | background-position: -992px 0
7 | }
8 | 100% {
9 | background-position: 992px 0
10 | }
11 | }
12 |
13 | .sans-serif {
14 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
15 | }
16 |
17 | .transparent {
18 | color: transparent;
19 | }
20 |
21 | .box-shadow-umi {
22 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
23 | }
24 |
25 | .emoji {
26 | height: 28px;
27 | user-select: none;
28 | }
29 |
30 | .tooltip {
31 | display: none;
32 | opacity: 0;
33 | transition: opacity .15s;
34 | pointer-events: none;
35 | padding: 4px;
36 | z-index: 2147483647;
37 | }
38 |
39 | .tooltip .tooltip-content {
40 | background: rgba(0, 0, 0, 0.6);
41 | color: white;
42 | padding: 5px 10px 4px;
43 | }
44 |
45 | .tooltip.tooltip-open-transitionend {
46 | display: block;
47 | }
48 |
49 | .tooltip.tooltip-after-open {
50 | opacity: 1;
51 | }
52 |
53 | .mal-icon {
54 | display: inline-block;
55 | width: 24px;
56 | padding: 2px;
57 | box-sizing: border-box;
58 | color: #004175;
59 | border: 0.125rem solid #004175;
60 | border-radius: 0.25rem;
61 | font-size: 12px;
62 | font-weight: bold;
63 | }
64 |
65 | .mal-icon:after {
66 | content: 'AL';
67 | }
68 |
69 | .mal-icon.watched {
70 | color: #19a974;
71 | border-color: #19a974;
72 | width: 44px;
73 | }
74 |
75 | .mal-icon.watched:after {
76 | content: 'AL ✔';
77 | }
78 |
79 | .player-top-offset {
80 | top: 64px;
81 | }
82 |
83 | .player-width {
84 | width: 1024px;
85 | }
86 |
87 | .player-height {
88 | height: 576px;
89 | }
90 |
91 | .container-width {
92 | width: 948px;
93 | }
94 |
95 |
96 | .pointer-events-none {
97 | pointer-events: none;
98 | }
99 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | You can't leave the player as a guest while in a room that's controlled by the host.
6 |
7 |
8 |
9 |
✅ Umi has updated successfully!
10 |
View changelog
11 |
Dismiss
12 |
13 |
14 |
❌ Something went wrong when contacting Crunchyroll.
15 |
Refresh
16 |
Dismiss
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Something went wrong when contacting Crunchyroll. This probably means it's the weekend and Crunchyroll's servers can't handle the load. Try waiting for a few seconds and refreshing.
27 |
28 |
Refresh
29 |
30 |
31 |
32 |
33 |
34 |
35 |
41 |
42 |
43 |
44 |
45 |
149 |
150 |
151 |
152 |
187 |
--------------------------------------------------------------------------------
/src/assets/emoji/FrostyFridays.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/remixz/umi/188084473e496026f5a996c401bd2599d6476580/src/assets/emoji/FrostyFridays.png
--------------------------------------------------------------------------------
/src/assets/emoji/TsuyuW.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/remixz/umi/188084473e496026f5a996c401bd2599d6476580/src/assets/emoji/TsuyuW.png
--------------------------------------------------------------------------------
/src/assets/emoji/UmiDorito.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/remixz/umi/188084473e496026f5a996c401bd2599d6476580/src/assets/emoji/UmiDorito.png
--------------------------------------------------------------------------------
/src/assets/emoji/Yousoro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/remixz/umi/188084473e496026f5a996c401bd2599d6476580/src/assets/emoji/Yousoro.png
--------------------------------------------------------------------------------
/src/assets/umi-login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/remixz/umi/188084473e496026f5a996c401bd2599d6476580/src/assets/umi-login.png
--------------------------------------------------------------------------------
/src/assets/umi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/remixz/umi/188084473e496026f5a996c401bd2599d6476580/src/assets/umi.png
--------------------------------------------------------------------------------
/src/components/Collection.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
53 |
--------------------------------------------------------------------------------
/src/components/DashboardLoadingSection.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
18 |
--------------------------------------------------------------------------------
/src/components/EpisodeScroller.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
27 |
--------------------------------------------------------------------------------
/src/components/Header.vue:
--------------------------------------------------------------------------------
1 |
2 |
85 |
86 |
87 |
184 |
185 |
371 |
--------------------------------------------------------------------------------
/src/components/LoadingMediaItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
58 |
--------------------------------------------------------------------------------
/src/components/MediaItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
29 |
30 |
31 |
32 |
51 |
52 |
144 |
--------------------------------------------------------------------------------
/src/components/QueueButton.vue:
--------------------------------------------------------------------------------
1 |
2 | {{inQueue ? 'Remove from queue' : 'Add to queue'}}
3 |
4 |
5 |
25 |
--------------------------------------------------------------------------------
/src/components/QueueItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
10 |
11 |
12 |
13 |
14 |
{{data.series.name}}
15 |
Next up: Episode {{data.most_likely_media.episode_number}} — {{data.most_likely_media.name}}
16 |
{{data.most_likely_media.description}}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
{{data.series.name}}
30 |
Next up: Episode {{data.most_likely_media.episode_number}}
31 |
Available to watch in {{parsedTime}}
32 |
33 |
34 |
35 |
36 |
37 |
54 |
55 |
72 |
--------------------------------------------------------------------------------
/src/components/Reactotron.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
27 |
--------------------------------------------------------------------------------
/src/components/Search.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
27 |
28 | {{series[id].name}}
29 |
30 |
31 |
32 |
33 |
34 |
148 |
149 |
192 |
--------------------------------------------------------------------------------
/src/components/SeriesItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 |
12 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/src/components/Video.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
373 |
374 |
404 |
405 |
427 |
--------------------------------------------------------------------------------
/src/lib/api.js:
--------------------------------------------------------------------------------
1 | import axios, {CancelToken} from 'axios'
2 |
3 | export const ACCESS_TOKEN = 'LNDJgOit5yaRIWN'
4 | export const DEVICE_TYPE = 'com.crunchyroll.windows.desktop'
5 | export const LOCALE = () => localStorage.getItem('locale') || 'enUS'
6 | export const VERSION = '1.1.20.0'
7 | export const CONNECTIVITY_TYPE = 'ethernet'
8 | export const UMI_SERVER = process.env.NODE_ENV === 'production' ? 'https://umi-watch-api.now.sh' : 'http://localhost:3001'
9 |
10 | let source = CancelToken.source()
11 |
12 | export default function api (opts) {
13 | const config = {
14 | method: opts.method || 'get',
15 | url: `https://api.crunchyroll.com/${opts.route}.${opts.version || '0'}.json`,
16 | params: !opts.data ? Object.assign({}, opts.params, {
17 | locale: LOCALE(),
18 | version: VERSION,
19 | connectivity_type: CONNECTIVITY_TYPE
20 | }) : null,
21 | data: opts.data,
22 | cancelToken: !opts.noCancel ? source.token : null
23 | }
24 |
25 | return axios(config)
26 | }
27 |
28 | export function cancelCurrentRequests () {
29 | source.cancel('User changed page')
30 | source = CancelToken.source()
31 | }
32 |
--------------------------------------------------------------------------------
/src/lib/auth.js:
--------------------------------------------------------------------------------
1 | import uuid from 'uuid/v4'
2 | import store from '../store'
3 |
4 | export function authGuard (to, from, next) {
5 | const loggedIn = !!store.state.auth.username
6 | if (loggedIn) {
7 | next()
8 | } else {
9 | next(`/login?next=${encodeURIComponent(to.fullPath)}`)
10 | }
11 | }
12 |
13 | export function loginGuard (to, from, next) {
14 | const noAuth = !store.state.auth.username
15 | if (noAuth) {
16 | next()
17 | } else {
18 | next('/')
19 | }
20 | }
21 |
22 | let localId = localStorage.getItem('umi-uuid')
23 | export function getUuid () {
24 | if (!localId) {
25 | localId = uuid().toUpperCase()
26 | localStorage.setItem('umi-uuid', localId)
27 | }
28 |
29 | return localId
30 | }
31 |
--------------------------------------------------------------------------------
/src/lib/bif.js:
--------------------------------------------------------------------------------
1 | // Based on https://github.com/chemoish/videojs-bif/blob/c6fdc0c2cfc9446927062995b7e8830ae45fff0d/src/parser.js
2 | import { fromByteArray } from 'base64-js'
3 |
4 | const BIF_INDEX_OFFSET = 64
5 | const FRAMEWISE_SEPARATION_OFFSET = 16
6 | const NUMBER_OF_BIF_IMAGES_OFFSET = 12
7 | const BIF_INDEX_ENTRY_LENGTH = 8
8 | const MAGIC_NUMBER = new Uint8Array([
9 | '0x89',
10 | '0x42',
11 | '0x49',
12 | '0x46',
13 | '0x0d',
14 | '0x0a',
15 | '0x1a',
16 | '0x0a'
17 | ])
18 |
19 | function validate (magicNumber) {
20 | return MAGIC_NUMBER.every((byte, i) => magicNumber[i] === byte)
21 | }
22 |
23 | export async function parse (url) {
24 | const res = await fetch(url.replace('https://img1.ak.crunchyroll.com/', '/cdn/'))
25 | const buf = await res.arrayBuffer()
26 |
27 | const magicNumber = new Uint8Array(buf).slice(0, 8)
28 | if (!validate(magicNumber)) {
29 | return []
30 | }
31 |
32 | const data = new DataView(buf)
33 | const framewiseSeparation = data.getUint32(FRAMEWISE_SEPARATION_OFFSET, true) || 1000
34 | const numberOfBIFImages = data.getUint32(NUMBER_OF_BIF_IMAGES_OFFSET, true)
35 |
36 | const bifData = []
37 | for (let i = 0, bifIndexEntryOffset = BIF_INDEX_OFFSET; i < numberOfBIFImages; i += 1, bifIndexEntryOffset += BIF_INDEX_ENTRY_LENGTH) {
38 | const bifIndexEntryTimestampOffset = bifIndexEntryOffset
39 | const bifIndexEntryAbsoluteOffset = bifIndexEntryOffset + 4
40 | const nextBifIndexEntryAbsoluteOffset = bifIndexEntryAbsoluteOffset + BIF_INDEX_ENTRY_LENGTH
41 |
42 | const offset = data.getUint32(bifIndexEntryAbsoluteOffset, true)
43 | const nextOffset = data.getUint32(nextBifIndexEntryAbsoluteOffset, true)
44 | const length = nextOffset - offset
45 |
46 | bifData.push({
47 | time: ((data.getUint32(bifIndexEntryTimestampOffset, true) * framewiseSeparation) / 1000) - 15,
48 | url: `data:image/jpeg;base64,${fromByteArray(new Uint8Array(buf.slice(offset, offset + length)))}`
49 | })
50 | }
51 |
52 | return bifData
53 | }
54 |
--------------------------------------------------------------------------------
/src/lib/cdnRewrite.js:
--------------------------------------------------------------------------------
1 | const CRUNCHYROLL_CDN = 'http://img1.ak.crunchyroll.com/'
2 |
3 | export default function cdnRewrite (url) {
4 | if (typeof url !== 'string') return url
5 |
6 | if (url.indexOf(CRUNCHYROLL_CDN) > -1) {
7 | return url.replace(CRUNCHYROLL_CDN, '/cdn/')
8 | } else {
9 | return url
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/lib/clappr-level-selector.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("Clappr")):"function"==typeof define&&define.amd?define(["Clappr"],t):"object"==typeof exports?exports.LevelSelector=t(require("Clappr")):e.LevelSelector=t(e.Clappr)}(this,function(e){return function(e){function t(n){if(l[n])return l[n].exports;var o=l[n]={exports:{},id:n,loaded:!1};return e[n].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var l={};return t.m=e,t.c=l,t.p="<%=baseUrl%>/",t(0)}([/*!******************!*\
3 | !*** ./index.js ***!
4 | \******************/
5 | function(e,t,l){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t["default"]=l(/*! ./src/main.js */1),e.exports=t["default"]},/*!*********************!*\
6 | !*** ./src/main.js ***!
7 | \*********************/
8 | function(e,t,l){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function r(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var i=function(){function e(e,t){for(var l=0;l0;t&&this.fillLevels(e.levels)}},{key:"reload",value:function(){this.unBindEvents(),this.bindEvents(),this.bindPlaybackEvents()}},{key:"shouldRender",value:function(){if(!this.core.getCurrentContainer())return!1;var e=this.core.getCurrentPlayback();if(!e)return!1;var t=void 0!==e.currentLevel,l=!!(this.levels&&this.levels.length>1);return t&&l}},{key:"render",value:function(){if(this.shouldRender()){var e=a.Styler.getStyleFor(h["default"],{baseUrl:this.core.options.baseUrl});this.$el.html(this.template({levels:this.levels,title:this.getTitle()})),this.$el.append(e),this.core.mediaControl.$(".media-control-right-panel").append(this.el),this.highlightCurrentLevel()}return this}},{key:"fillLevels",value:function(e){var t=arguments.length<=1||void 0===arguments[1]?f:arguments[1];void 0===this.selectedLevelId&&(this.selectedLevelId=t),this.levels=e,this.configureLevelsLabels(),this.render()}},{key:"configureLevelsLabels",value:function(){if(void 0!==this.core.options.levelSelectorConfig){var e=this.core.options.levelSelectorConfig.labelCallback;if(e&&"function"!=typeof e)throw new TypeError("labelCallback must be a function");var t=this.core.options.levelSelectorConfig.labels,l=t?this.core.options.levelSelectorConfig.labels:{};if(e||t){var n,o;for(var r in this.levels)n=this.levels[r],o=l[n.id],e?n.label=e(n,o):o&&(n.label=o)}}}},{key:"findLevelBy",value:function(e){var t;return this.levels.forEach(function(l){l.id===e&&(t=l)}),t}},{key:"onLevelSelect",value:function(e){return this.selectedLevelId=parseInt(e.target.dataset.levelSelectorSelect,10),this.core.getCurrentPlayback().currentLevel!=this.selectedLevelId&&(this.core.getCurrentPlayback().currentLevel=this.selectedLevelId,this.toggleContextMenu(),e.stopPropagation(),!1)}},{key:"onShowLevelSelectMenu",value:function(e){this.toggleContextMenu()}},{key:"hideSelectLevelMenu",value:function(){this.$(".level_selector ul").hide()}},{key:"toggleContextMenu",value:function(){this.$(".level_selector ul").toggle()}},{key:"buttonElement",value:function(){return this.$(".level_selector button")}},{key:"levelElement",value:function(e){return this.$(".level_selector ul a"+(isNaN(e)?"":'[data-level-selector-select="'+e+'"]')).parent()}},{key:"getTitle",value:function(){return(this.core.options.levelSelectorConfig||{}).title}},{key:"startLevelSwitch",value:function(){this.buttonElement().addClass("changing")}},{key:"stopLevelSwitch",value:function(){this.buttonElement().removeClass("changing")}},{key:"updateText",value:function(e){e===f?this.buttonElement().text(this.currentLevel?"AUTO ("+this.currentLevel.label+")":"AUTO"):this.buttonElement().text(this.findLevelBy(e).label)}},{key:"updateCurrentLevel",value:function(e){var t=this.findLevelBy(e.level);this.currentLevel=t?t:null,this.highlightCurrentLevel()}},{key:"highlightCurrentLevel",value:function(){this.levelElement().removeClass("current"),this.currentLevel&&this.levelElement(this.currentLevel.id).addClass("current"),this.updateText(this.selectedLevelId)}},{key:"name",get:function(){return"level_selector"}},{key:"template",get:function(){return(0,a.template)(u["default"])}},{key:"attributes",get:function(){return{"class":this.name,"data-level-selector":""}}},{key:"events",get:function(){return{"click [data-level-selector-select]":"onLevelSelect","click [data-level-selector-button]":"onShowLevelSelectMenu"}}}],[{key:"version",get:function(){return VERSION}}]),t}(a.UICorePlugin);t["default"]=d,e.exports=t["default"]},/*!*************************!*\
9 | !*** external "Clappr" ***!
10 | \*************************/
11 | function(t,l){t.exports=e},/*!****************************************!*\
12 | !*** ./src/public/level-selector.html ***!
13 | \****************************************/
14 | function(e,t){e.exports='\n Auto\n \n\n <% if (title) { %>\n <%= title %> \n <% }; %>\n AUTO \n <% for (var i = 0; i < levels.length; i++) { %>\n <%= levels[i].label %> \n <% }; %>\n \n'},/*!*******************************!*\
15 | !*** ./src/public/style.scss ***!
16 | \*******************************/
17 | function(e,t,l){t=e.exports=l(/*! ./../../~/css-loader/lib/css-base.js */5)(),t.push([e.id,".level_selector[data-level-selector]{float:right;margin-top:5px;position:relative}.level_selector[data-level-selector] button{background-color:transparent;color:#fff;font-family:Roboto,Open Sans,Arial,sans-serif;-webkit-font-smoothing:antialiased;border:none;font-size:10px}.level_selector[data-level-selector] button:hover{color:#c9c9c9}.level_selector[data-level-selector] button.changing{-webkit-animation:pulse .5s infinite alternate}.level_selector[data-level-selector]>ul{list-style-type:none;position:absolute;bottom:25px;border:1px solid #000;display:none;background-color:#e6e6e6}.level_selector[data-level-selector] li{font-size:10px}.level_selector[data-level-selector] li[data-title]{background-color:#c3c2c2;padding:5px}.level_selector[data-level-selector] li a{color:#444;padding:2px 10px;display:block;text-decoration:none}.level_selector[data-level-selector] li a:hover{background-color:#555;color:#fff}.level_selector[data-level-selector] li a:hover a{color:#fff;text-decoration:none}.level_selector[data-level-selector] li.current a{color:red}@-webkit-keyframes pulse{0%{color:#fff}50%{color:#ff0101}to{color:#b80000}}",""])},/*!**************************************!*\
18 | !*** ./~/css-loader/lib/css-base.js ***!
19 | \**************************************/
20 | function(e,t){"use strict";e.exports=function(){var e=[];return e.toString=function(){for(var e=[],t=0;t {
22 | try {
23 | if (!this.app) this.app = firebase.initializeApp(config)
24 | await this.app.auth().signInAnonymously()
25 | this.connected = true
26 | resolve()
27 | } catch (err) {
28 | reject(err)
29 | }
30 | })
31 | },
32 |
33 | getRef (str) {
34 | if (!this.refs[str]) {
35 | this.refs[str] = this.app.database().ref(str)
36 | }
37 |
38 | return this.refs[str]
39 | },
40 |
41 | install (Vue) {
42 | Object.defineProperty(Vue.prototype, '$firebase', {
43 | get () {
44 | return Firebase
45 | }
46 | })
47 | }
48 | }
49 |
50 | export default Firebase
51 |
--------------------------------------------------------------------------------
/src/lib/prettyTime.js:
--------------------------------------------------------------------------------
1 | export default function prettyTime (time) {
2 | const mins = Math.floor(time / 60)
3 | const secs = time - (mins * 60)
4 |
5 | return `${mins}:${secs < 10 ? '0' : ''}${secs}`
6 | }
7 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill'
2 | import Vue from 'vue'
3 | import App from './App'
4 | import router from './router'
5 | import store from './store'
6 | import { sync } from 'vuex-router-sync'
7 | import Firebase from 'lib/firebase'
8 | import VTooltip from 'v-tooltip'
9 | import cdnRewrite from 'lib/cdnRewrite'
10 |
11 | Vue.use(VTooltip)
12 | Vue.use(Firebase)
13 | sync(store, router)
14 | Vue.filter('cdnRewrite', cdnRewrite)
15 |
16 | /* eslint-disable no-new */
17 | new Vue({
18 | el: '#app',
19 | router,
20 | store,
21 | render: h => h(App)
22 | })
23 |
24 | if (process.env.NODE_ENV === 'production') {
25 | const runtime = require('offline-plugin/runtime')
26 | runtime.install({
27 | onUpdateReady () {
28 | runtime.applyUpdate()
29 | },
30 | onUpdated () {
31 | localStorage.setItem('updated', Date.now())
32 | location.reload()
33 | }
34 | })
35 | }
36 |
--------------------------------------------------------------------------------
/src/pages/Changelog.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Changelog
4 |
5 |
2020-08-27: This thing is still on? (Umi is working again!)
6 |
7 |
8 |
9 | No, really? Hi! It's been a while. In fact, we probably haven't met, since I stopped updating this in 2018, so you've never seen this page. However, a few of you reached out to me, and said Umi broke recently. So, I've fixed it. No extension or anything needed, it should, hopefully, just work.
10 |
11 |
12 | I just took a look at the analytics, and turns out a lot of you started using this during the pandemic. Let me know on GitHub or Twitter if anything else breaks. Cheers!
13 |
14 |
15 |
16 |
17 |
2018-10-09: AniList integration
18 |
19 |
20 |
21 | You've probably noticed that the MyAnimeList integration hasn't been working for a while. That's because MyAnimeList shuttered their API until further notice, due to some security issues. As it still seems like it's a ways off until MyAnimeList's API is working again, I decided to integrate AniList support into Umi. It's actually a bit nicer than the MyAnimeList integration, since you do the sign-in on AniList's site, which is a lot safer for you. I hope to add back MyAnimeList once they release their new, safer API.
22 |
23 |
24 | Fixed an issue where an episode's timeline thumbnails weren't loading.
25 |
26 |
27 | As an aside: Umi hasn't been updated in a bit because I haven't been using it nearly as much, as I haven't been watching that much anime. Thankfully, Umi has been in a pretty good state since the last update. It's still a lot faster than Crunchyroll's site, even with their HTML5 player, and there haven't been any major issues. I have no timeline for any more major additions, but if inspiration strikes, you may see some more updates soon. I will make a dark mode happen at some point!
28 |
29 |
30 |
31 |
32 |
2018-03-27: All series work again!!
33 |
34 |
35 |
36 | If you've tried to use Umi for the last few weeks, you'll have noticed that new series and episodes stopped working. I initially thought this was unfixable, and published a letter wishing you all well. Well... hopefully you've come back for some reason, as I figured out a way to get it working again! It's not absolutely perfect, as it requires you to install an extension for Chrome or Firefox . However, unlike many other extensions that hook into Crunchyroll, I don't need to grab your credentials in the extension, so it satisfies my personal safety requirements. (The quick technical rundown is that it just changes the CORS headers on the incoming video responses.)
37 |
38 |
39 | In other news, I teased to some people that I may work on a desktop app of Umi that also works with sites like Funimation and HiDive. I'm still planning to work on this eventually! I may also be able to make it work in the browser as well, thanks to this Chrome extension, but I wouldn't hold my breath on it. I wanted to release this extension now though, so that people can continue to use Umi. Happy watching!
40 |
41 |
42 |
43 |
44 |
2018-02-03: Video player thumbnails & improvements
45 |
46 |
47 |
48 | When you hover across the timeline of a video, previews of the video at that time will appear!
49 |
50 |
51 | You'll no longer get any HTTPS errors when using Umi. All communication to the Crunchyroll servers was done over HTTPS, but stuff like the posters for a series or thumbnails of an episode were over HTTP, as Crunchyroll only serves them over HTTP. (2018!) I'm now proxying their CDN, so it will be HTTPS.
52 |
53 |
54 | Dropped support for some older browsers, which allows the code to be a bit smaller and hopefully a tiny bit faster.
55 |
56 |
57 | Fix a bug with the message shown in host-only mode showing if you overscrolled upwards (possible with a trackpad on Mac/iOS, maybe Windows too but I only have a desktop PC).
58 |
59 |
60 |
61 |
62 |
2018-01-23: Host-only mode for rooms
63 |
64 |
65 |
66 | As the title implies, rooms now have a new feature: Host-only mode. If you create a room, you'll now see a checkbox for this mode in the room popup. When toggled on, only you (the host) will be able to play/pause/change episode/etc. This is useful if you're doing a large group watch, and don't want people accidentally (or purposely 😈) messing with the video. It might still be a little rough around the edges, so if you find any bugs, please let me know on GitHub . Happy watching!
67 |
68 |
69 |
70 |
71 |
2017-12-08: Minor updates
72 |
73 |
74 |
75 | Umi will now prompt you if you need to sign in again due to your session expiring. A session might expire if you change your password, or if you don't use Umi for a little while.
76 |
77 | This fixes a bug where your queue and history data wouldn't load properly.
78 |
79 |
80 |
81 | Improve player time syncing in rooms.
82 |
83 |
84 | Fix bug where language settings wouldn't load properly.
85 |
86 |
87 | Fix reaction selector being blurry when in fullscreen.
88 |
89 |
90 | Minor design updates.
91 |
92 |
93 | Happy holidays!
94 |
95 |
96 |
97 |
98 |
2017-11-30: Rooms rewrite & more
99 |
100 |
101 |
102 | Rooms rewrite:
103 | The rooms system has been completely rewritten from the ground up. You can read the technical details here if you're interested, but the gist of it is that the rooms are going to be much more reliable. One of the biggest changes as a result of this is that rooms no longer expire. This means you can now reuse old links! The link won't remember what you're watching last once everyone has left the room, but now you and your friends can just join a link whenever. This update also sets up Umi for some future improvements, like moderators, private rooms, custom links, and more.
104 |
105 |
106 | Improved update system:
107 | TL;DR: Updates are now installed automatically. No more ugly yellow bar.
108 |
109 | If you've been using Umi for a while, you've probably seen this ugly bar pop up every once in a while:
110 |
111 |
An update is ready to be installed.
112 |
113 |
114 | nico nico nii~~
115 |
116 |
Install and refresh
117 |
118 |
119 | As a brief explanation of why this would happen: To make Umi even faster, the website stores a copy of all its code on your device, so it doesn't need to contact the Umi servers to load. Since the website doesn't contact the server when you first open it, the website has to check for updates after it has loaded. This is when you'd get the update notification.
120 |
121 | I setup the old system to continually check for updates, including while you were using the player. Part of this was because of the old rooms system, as it was very fickle with people leaving, so I didn't want to make it so auto-update while you in a room. Now that rooms are a lot more stable, Umi will just check for updates when you first open the site, and automatically refresh if there's an update. You'll get a notification that links you here for details on the update.
122 |
123 |
124 | What's next:
125 | I'm generally pretty happy with how Umi has progressed so far, and I hope you're enjoying using it. I do still have some ideas for the future though! Here are some, in no particular order of adding them:
126 |
127 |
128 | Spoilers mode (hides all thumbnails and episode titles)
129 |
130 |
131 | Improved rooms (moderators, private rooms, custom links)
132 |
133 |
134 | Support for additional streaming sites
135 |
136 |
137 | Offline support (if Crunchyroll adds it; technically Umi will load now if you're offline)
138 |
139 |
140 | Improved reactions (choose custom reactions instead of the default set)
141 |
142 |
143 | If you have any suggestions or ideas for Umi, feel free to add them as issues on GitHub. Happy watching!
144 |
145 |
146 |
147 |
148 |
149 |
150 |
163 |
164 |
197 |
--------------------------------------------------------------------------------
/src/pages/Dashboard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Continue watching
6 |
10 | View more
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
Your queue
21 |
25 | View more
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
Latest releases
35 |
36 |
37 |
{{title}}
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
124 |
125 |
130 |
--------------------------------------------------------------------------------
/src/pages/History.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{paginationLoading ? 'Loading...' : 'Load more'}}
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
54 |
55 |
60 |
--------------------------------------------------------------------------------
/src/pages/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
UMI!
5 |
The improved Crunchyroll player.
6 |
Watch together online, sync to AniList, and more.
7 |
28 |
29 |
30 |
31 |
32 |
33 |
74 |
75 |
102 |
--------------------------------------------------------------------------------
/src/pages/Media.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Resume watching at {{prettyTime}}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | Watch next episode: Episode {{nextEpisodeMedia.episode_number}} — {{nextEpisodeMedia.name}}
22 |
23 |
24 |
25 |
26 |
27 |
33 |
56 |
57 |
68 |
69 |
70 |
71 |
333 |
334 |
376 |
--------------------------------------------------------------------------------
/src/pages/Migrate.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
33 |
--------------------------------------------------------------------------------
/src/pages/Notice.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
The Future of Umi
4 |
March 21st, 2018
5 |
6 |
7 |
8 | Hi there! I'm Zach, the guy who created this site.
9 |
10 |
11 | If you've tried to use Umi player in the last couple weeks, you've probably noticed that new episodes of anime aren't working anymore. Unfortunately, I must bear some bad news: Crunchyroll has started to secure to their new content in a way that I don't believe I can work around. As Crunchyroll inevitably migrates more content to this system, Umi will slowly stop working. You can read the technical details here: https://github.com/remixz/umi/issues/18#issuecomment-373978176
12 |
13 |
14 | I'm not especially surprised this has happened now. A pirate site was released that worked in a very similar way to Umi, except that it required no account to log in, and would just steal the videos directly from Crunchyroll servers. In other words, these pirates could rehost anime at a decently high quality without paying anything in hosting, while adding their own advertisements. From a pirate's perspective, that's pretty much perfect. With that in mind, I definitely don't blame Crunchyroll for taking this action now. If anything, it should have been done sooner!
15 |
16 |
17 | To be honest, I'm not too sad that Crunchyroll is starting to secure their services, as I knew it would be inevitable. What I'm a bit more sad about is that so many people use this site (about one thousand every month!), presumably to watch anime with friends, family and others (though maybe they just like my amazing UI design 😉) and now won't be able to. Umi started as a fun passion project for me and my friends. I posted it to the MyAnimeList forums, but it didn't get any replies for months, so I assumed no one would use it, which is understandable due to it being non-official but still using Crunchyroll credentials. I was definitely surprised to see the numbers I did, and how it's grown pretty significantly every month. My favourite part was people sending me tweets (even if I didn't respond every time) or GitHub issues or emails, saying that they used it to watch anime with someone else. In its own weird way, it was a little community! I think that's so cool.
18 |
19 |
20 | I hope that maybe one day in the future, Crunchyroll will add a watch together feature natively. I believe it could be done while keeping their existing license terms with distributors, as they'd just need to enforce one streaming session for a specific episode for an account. That way, there'd be no balking over sharing accounts. It would just be two separate accounts (and therefore two separate revenue streams) streaming the same episode at the same time, something that happens all the time. I made this letter readable without logging in, so you can send it to Crunchyroll or something if you'd really like to nag them.
21 |
22 |
23 | So, consider this letter not only an explanation of what's going on, but as a big thank you to everyone who used Umi! I pretty much developed it for myself and my friends in mind most of the time (they chose all the reaction faces!), but it was really great to know that other people used it to have some fun together. As for what's next, I'll be leaving up Umi for the foreseeable future, since it still works for some content, and on mobile since it seems iOS and Android just ignore the security for videos. As well, when I bought the domain, there was a 5 years for 5 dollars promotion, so no reason to take it down. 😂 If I can fix Umi to start working again, I will, and maybe update this letter, or just remove it and pretend it never happened. As well, I may work on a downloadable version of Umi, which would let me ignore the security Crunchyroll has put in to stop it from working in the browser. I understand if people won't want to use that though, since it probably feels a bit less safe.
24 |
25 |
26 | If you have questions, comments, or anything else, please feel free to send them my way on Twitter (@zachbruggeman) , or send me an email. And if you work at Crunchyroll: Thanks for not breaking things until you had to because of pirates. 😂 I'd love to talk about some ideas, so email me if you're interested. 😉
27 |
28 |
29 | Thanks for reading this, and happy watching!
30 |
31 |
Zach Bruggeman
32 |
33 |
34 |
35 |
36 |
37 |
45 |
--------------------------------------------------------------------------------
/src/pages/Queue.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Recently updated
6 |
Queue order
7 |
8 |
9 |
10 |
11 |
12 |
22 |
23 |
24 |
25 |
67 |
68 |
104 |
--------------------------------------------------------------------------------
/src/pages/Room.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Joining room...
4 |
5 |
6 |
7 |
35 |
--------------------------------------------------------------------------------
/src/pages/Search.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Loading...
5 | {{searchIds.length}} result{{searchIds.length !== 1 ? 's' : ''}}
6 |
7 |
8 |
9 |
Loading...
10 | No results found.
11 |
12 |
13 |
14 |
15 |
72 |
73 |
78 |
--------------------------------------------------------------------------------
/src/pages/Series.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
This series is not available.
5 |
6 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
{{series.name || ' '}}
18 |
{{series.description}}
19 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
Oldest
31 |
Newest
32 |
33 |
34 |
35 |
36 |
37 |
38 | {{collectionData[id].name}}
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
185 |
186 |
223 |
--------------------------------------------------------------------------------
/src/pages/Settings.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Profile
4 |
5 |
6 | Content language
7 |
8 |
9 |
10 | {{locale.label}}
11 |
12 |
Saved!
13 |
14 |
15 |
Connections
16 |
17 |
18 |
19 | AniList
20 | ({{alAuth.name}})
21 |
22 |
23 | Authenticating...
24 |
25 |
28 |
29 | Sign out
30 |
31 |
32 |
33 |
34 |
35 |
123 |
124 |
129 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 | import Meta from 'vue-meta'
4 | import Analytics from 'vue-analytics'
5 | import store from '../store'
6 | import {authGuard, loginGuard} from 'lib/auth'
7 | import {cancelCurrentRequests} from 'lib/api'
8 |
9 | import Dashboard from 'pages/Dashboard'
10 | import Migrate from 'pages/Migrate'
11 | import Queue from 'pages/Queue'
12 | import Settings from 'pages/Settings'
13 | import History from 'pages/History'
14 | import Search from 'pages/Search'
15 | import Login from 'pages/Login'
16 | import Series from 'pages/Series'
17 | import Media from 'pages/Media'
18 | import Room from 'pages/Room'
19 | import Changelog from 'pages/Changelog'
20 | import Notice from 'pages/Notice'
21 |
22 | Vue.use(Router)
23 | Vue.use(Meta)
24 |
25 | const router = new Router({
26 | mode: 'history',
27 | scrollBehavior (to, from, savedPosition) {
28 | if (savedPosition) {
29 | return savedPosition
30 | } else {
31 | return { x: 0, y: 0 }
32 | }
33 | },
34 | routes: [
35 | {
36 | path: '/',
37 | name: 'dashboard',
38 | component: Dashboard,
39 | beforeEnter: authGuard
40 | },
41 | {
42 | path: '/migrate',
43 | name: 'migrate',
44 | component: Migrate
45 | },
46 | {
47 | path: '/queue',
48 | name: 'queue',
49 | component: Queue,
50 | beforeEnter: authGuard
51 | },
52 | {
53 | path: '/settings',
54 | name: 'settings',
55 | component: Settings,
56 | beforeEnter: authGuard
57 | },
58 | {
59 | path: '/history',
60 | name: 'history',
61 | component: History,
62 | beforeEnter: authGuard
63 | },
64 | {
65 | path: '/login',
66 | name: 'login',
67 | component: Login,
68 | beforeEnter: loginGuard
69 | },
70 | {
71 | path: '/search',
72 | name: 'search',
73 | component: Search,
74 | beforeEnter: authGuard
75 | },
76 | {
77 | path: '/series/:id',
78 | name: 'series',
79 | component: Series,
80 | beforeEnter: authGuard
81 | },
82 | {
83 | path: '/series/:seriesId/:id',
84 | name: 'media',
85 | component: Media,
86 | beforeEnter: authGuard
87 | },
88 | {
89 | path: '/room/:id',
90 | name: 'room',
91 | component: Room,
92 | beforeEnter: authGuard
93 | },
94 | {
95 | path: '/changelog',
96 | name: 'changelog',
97 | component: Changelog,
98 | beforeEnter: authGuard
99 | },
100 | {
101 | path: '/future-of-umi',
102 | name: 'future-of-umi',
103 | component: Notice
104 | }
105 | ]
106 | })
107 |
108 | router.beforeEach((to, from, next) => {
109 | // cancel navigation when in host-only mode and client is not the host
110 | if (store.state.roomConnected && store.state.roomData.hostOnly && !store.getters.isRoomHost && to.name !== from.name) {
111 | store.dispatch('flashGuestMessage')
112 | return next(false)
113 | }
114 |
115 | // on all page changes aside from the initial load, we cancel in progress requests
116 | if (from.name && from.name !== to.name) {
117 | cancelCurrentRequests()
118 | }
119 | next()
120 | })
121 |
122 | if (process.env.NODE_ENV === 'production') {
123 | Vue.use(Analytics, {id: 'UA-46859303-4', router})
124 | }
125 |
126 | export default router
127 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import uuid from 'uuid/v4'
4 | import axios, {isCancel} from 'axios'
5 |
6 | import api, {ACCESS_TOKEN, DEVICE_TYPE, LOCALE, VERSION, UMI_SERVER} from 'lib/api'
7 | import {getUuid} from 'lib/auth'
8 | import Firebase from 'lib/firebase'
9 |
10 | const MEDIA_FIELDS = 'media.media_id,media.available,media.available_time,media.collection_id,media.collection_name,media.series_id,media.type,media.episode_number,media.name,media.description,media.screenshot_image,media.created,media.duration,media.playhead,media.bif_url'
11 | const SERIES_FIELDS = 'series.series_id,series.name,series.portrait_image,series.landscape_image,series.description,series.in_queue'
12 |
13 | Vue.use(Vuex)
14 |
15 | function handleError (err, reject) {
16 | if (!isCancel(err)) {
17 | store.commit('SET_ERROR', true)
18 | reject(err)
19 | }
20 | }
21 |
22 | const ANILIST_AUTH_QUERY = `
23 | query {
24 | Viewer {
25 | name
26 | }
27 | }
28 | `
29 |
30 | const store = new Vuex.Store({
31 | state: {
32 | auth: localStorage.getItem('auth') ? (
33 | JSON.parse(localStorage.getItem('auth'))
34 | ) : (
35 | {}
36 | ),
37 | malAuth: localStorage.getItem('malAuth') ? (
38 | JSON.parse(localStorage.getItem('malAuth'))
39 | ) : (
40 | {}
41 | ),
42 | alAuth: localStorage.getItem('alAuth') ? (
43 | JSON.parse(localStorage.getItem('alAuth'))
44 | ) : (
45 | {}
46 | ),
47 | locales: [],
48 | series: {},
49 | seriesCollections: {},
50 | collections: {},
51 | collectionMedia: {},
52 | media: {},
53 | searchIds: [],
54 | searchQuery: '',
55 | queueData: [],
56 | initialHistory: [],
57 | recent: [],
58 | roomId: '',
59 | roomConnected: false,
60 | roomMenu: false,
61 | roomData: {},
62 | connectedCount: 0,
63 | lights: false,
64 | error: false,
65 | expiredSession: '',
66 | guestMessage: false,
67 | readExtension: localStorage.getItem('readExtension') ? true : false
68 | },
69 |
70 | actions: {
71 | startSession ({commit, state}) {
72 | const params = {
73 | access_token: ACCESS_TOKEN,
74 | device_type: DEVICE_TYPE,
75 | device_id: getUuid()
76 | }
77 |
78 | if (state.auth.token) {
79 | params.auth = state.auth.token
80 | }
81 |
82 | return new Promise(async (resolve, reject) => {
83 | try {
84 | const resp = await api({route: 'start_session', params, noCancel: true})
85 | const data = resp.data.data
86 | commit('UPDATE_AUTH', {
87 | session_id: data.session_id,
88 | country: data.country_code.toLowerCase(),
89 | token: data.auth,
90 | expires: data.expires
91 | })
92 | resolve()
93 | // fetch locales in the background
94 | const localeResp = await api({
95 | route: 'list_locales',
96 | version: '1',
97 | params: {session_id: data.session_id},
98 | noCancel: true
99 | })
100 | commit('UPDATE_LOCALES', localeResp.data.data.locales)
101 | } catch (err) {
102 | reject(err)
103 | }
104 | })
105 | },
106 |
107 | login ({commit, state, dispatch}, {username, password}) {
108 | const form = new FormData()
109 | form.append('account', username)
110 | form.append('password', password)
111 | form.append('session_id', state.auth.session_id)
112 | form.append('locale', LOCALE())
113 | form.append('version', VERSION)
114 |
115 | return new Promise(async (resolve, reject) => {
116 | try {
117 | const resp = await api({method: 'post', route: 'login', data: form})
118 | if (resp.data.error) throw resp
119 |
120 | const data = resp.data.data
121 | if (data.user.premium.indexOf('anime') === -1) {
122 | return reject(new Error('You must have a premium Crunchyroll account to use Umi.'))
123 | }
124 | commit('UPDATE_AUTH', {
125 | token: data.auth,
126 | expires: data.expires,
127 | username: data.user.username
128 | })
129 | commit('SET_EXPIRED_SESSION', '')
130 | resolve()
131 | } catch (err) {
132 | reject(err)
133 | }
134 | })
135 | },
136 |
137 | logout ({commit, dispatch, state}, didExpire) {
138 | return new Promise(async (resolve, reject) => {
139 | try {
140 | if (didExpire) {
141 | commit('SET_EXPIRED_SESSION', state.auth.username)
142 | }
143 | commit('REMOVE_AUTH')
144 | await dispatch('startSession')
145 | resolve()
146 | commit('SET_INITIAL_HISTORY', [])
147 | commit('SET_QUEUE_DATA', [])
148 | } catch (err) {
149 | handleError(err, reject)
150 | }
151 | })
152 | },
153 |
154 | authenticateMal ({commit, state}, {username, password}) {
155 | return new Promise(async (resolve, reject) => {
156 | try {
157 | const {data} = await axios.post(`${UMI_SERVER}/mal/login`, {username, password})
158 | if (data.status === 'ok') {
159 | commit('UPDATE_MAL', data)
160 | resolve()
161 | } else {
162 | reject(new Error(data.error))
163 | }
164 | } catch (err) {
165 | reject(err)
166 | }
167 | })
168 | },
169 |
170 | authenticateAniList ({commit}, {token}) {
171 | return new Promise(async (resolve, reject) => {
172 | try {
173 | const {data: {data, errors}} = await axios({
174 | method: 'post',
175 | url: 'https://graphql.anilist.co',
176 | headers: {
177 | Authorization: `Bearer ${token}`
178 | },
179 | data: {
180 | query: ANILIST_AUTH_QUERY
181 | }
182 | })
183 | if (errors && errors.length > 0) {
184 | reject(new Error(errors[0]))
185 | } else {
186 | console.log(data)
187 | commit('UPDATE_AL', {
188 | token,
189 | name: data.Viewer.name
190 | })
191 | resolve()
192 | }
193 | } catch (err) {
194 | reject(err)
195 | }
196 | })
197 | },
198 |
199 | getQueueInfo ({commit, state}, force) {
200 | const params = {
201 | session_id: state.auth.session_id,
202 | media_types: 'anime|drama',
203 | fields: [MEDIA_FIELDS, SERIES_FIELDS].join(',')
204 | }
205 |
206 | if (state.queueData.length > 0 && !force) return Promise.resolve()
207 |
208 | return new Promise(async (resolve, reject) => {
209 | try {
210 | const resp = await api({route: 'queue', params})
211 | if (resp.data.error) throw resp
212 |
213 | const data = resp.data.data
214 | commit('SET_QUEUE_DATA', data)
215 | data.forEach((d) => {
216 | commit('ADD_SERIES', d.series)
217 | commit('ADD_MEDIA', d.most_likely_media)
218 | commit('ADD_MEDIA', d.last_watched_media)
219 | })
220 | resolve()
221 | } catch (err) {
222 | handleError(err, reject)
223 | }
224 | })
225 | },
226 |
227 | getHistoryInfo ({commit, state}, {limit = 24, offset = 0} = {}) {
228 | const params = {
229 | session_id: state.auth.session_id,
230 | media_types: 'anime|drama',
231 | fields: [MEDIA_FIELDS, SERIES_FIELDS].join(','),
232 | limit,
233 | offset
234 | }
235 |
236 | return new Promise(async (resolve, reject) => {
237 | try {
238 | const resp = await api({route: 'recently_watched', params})
239 | if (resp.data.error) throw resp
240 |
241 | const data = resp.data.data
242 | if (offset === 0) {
243 | commit('SET_INITIAL_HISTORY', data)
244 | }
245 | data.forEach((d) => {
246 | commit('ADD_SERIES', d.series)
247 | commit('ADD_COLLECTION', d.collection)
248 | commit('ADD_MEDIA', d.media)
249 | })
250 | resolve(data)
251 | } catch (err) {
252 | handleError(err, reject)
253 | }
254 | })
255 | },
256 |
257 | getRecentInfo ({commit, state}) {
258 | const params = {
259 | session_id: state.auth.session_id,
260 | media_type: 'anime',
261 | fields: [MEDIA_FIELDS, 'media.series_name', 'series.most_recent_media'].join(','),
262 | limit: 50,
263 | offset: 0,
264 | filter: 'updated'
265 | }
266 |
267 | if (state.recent.length > 0) return Promise.resolve(state.recent)
268 |
269 | return new Promise(async (resolve, reject) => {
270 | try {
271 | const resp = await api({route: 'list_series', params})
272 | if (resp.data.error) throw resp
273 |
274 | const data = resp.data.data
275 | data.forEach((d) => {
276 | commit('ADD_MEDIA', d.most_recent_media)
277 | })
278 | commit('SET_RECENT', data)
279 | resolve(data)
280 | } catch (err) {
281 | handleError(err, reject)
282 | }
283 | })
284 | },
285 |
286 | search ({commit, state}, q) {
287 | const params = {
288 | session_id: state.auth.session_id,
289 | classes: 'series',
290 | limit: 999,
291 | offset: 0,
292 | media_types: 'anime|drama',
293 | fields: SERIES_FIELDS,
294 | q
295 | }
296 |
297 | return new Promise(async (resolve, reject) => {
298 | try {
299 | const resp = await api({route: 'autocomplete', params})
300 | if (resp.data.error) throw resp
301 |
302 | const data = resp.data.data
303 | data.forEach((d) => {
304 | commit('ADD_SERIES', d)
305 | })
306 | commit('SET_SEARCH_IDS', data.map((d) => d.series_id))
307 | resolve()
308 | } catch (err) {
309 | handleError(err, reject)
310 | }
311 | })
312 | },
313 |
314 | getSeriesInfo ({commit, state}, id) {
315 | const params = {
316 | session_id: state.auth.session_id,
317 | series_id: id,
318 | fields: SERIES_FIELDS
319 | }
320 |
321 | if (state.series[id]) return Promise.resolve()
322 |
323 | return new Promise(async (resolve, reject) => {
324 | try {
325 | const resp = await api({route: 'info', params})
326 | if (resp.data.error) throw resp
327 |
328 | const data = resp.data.data
329 | commit('ADD_SERIES', data)
330 | resolve()
331 | } catch (err) {
332 | handleError(err, reject)
333 | }
334 | })
335 | },
336 |
337 | updateSeriesQueue ({commit, state, dispatch}, {id, queueStatus}) {
338 | const form = new FormData()
339 | form.append('session_id', state.auth.session_id)
340 | form.append('locale', LOCALE())
341 | form.append('version', VERSION)
342 | form.append('series_id', id)
343 |
344 | commit('UPDATE_SERIES_QUEUE', {id, queueStatus})
345 |
346 | return new Promise(async (resolve, reject) => {
347 | try {
348 | const resp = await api({method: 'post', route: queueStatus ? 'add_to_queue' : 'remove_from_queue', data: form})
349 | if (resp.data.error) throw resp
350 |
351 | dispatch('getQueueInfo', true)
352 | resolve()
353 | } catch (err) {
354 | handleError(err, reject)
355 | }
356 | })
357 | },
358 |
359 | getCollectionsForSeries ({commit, state}, id) {
360 | const params = {
361 | session_id: state.auth.session_id,
362 | series_id: id,
363 | limit: 5000,
364 | offset: 0
365 | }
366 |
367 | if (state.seriesCollections[id]) return Promise.resolve()
368 |
369 | return new Promise(async (resolve, reject) => {
370 | try {
371 | const resp = await api({route: 'list_collections', params})
372 | if (resp.data.error) throw resp
373 |
374 | const data = resp.data.data
375 | data.forEach((d) => {
376 | commit('ADD_COLLECTION', d)
377 | })
378 | commit('ADD_SERIES_COLLECTION', {id, arr: data.map((d) => d.collection_id)})
379 | resolve()
380 | } catch (err) {
381 | handleError(err, reject)
382 | }
383 | })
384 | },
385 |
386 | getMediaForCollection ({commit, state}, id) {
387 | const params = {
388 | session_id: state.auth.session_id,
389 | collection_id: id,
390 | include_clips: 1,
391 | limit: 5000,
392 | offset: 0,
393 | fields: MEDIA_FIELDS
394 | }
395 |
396 | if (state.collectionMedia[id]) return Promise.resolve()
397 |
398 | return new Promise(async (resolve, reject) => {
399 | try {
400 | const resp = await api({route: 'list_media', params})
401 | if (resp.data.error) throw resp
402 |
403 | const data = resp.data.data
404 | data.forEach((d) => {
405 | commit('ADD_MEDIA', d)
406 | })
407 | commit('ADD_COLLECTION_MEDIA', {id, arr: data.map((d) => d.media_id)})
408 | resolve()
409 | } catch (err) {
410 | handleError(err, reject)
411 | }
412 | })
413 | },
414 |
415 | getCollectionInfo ({commit, state}, id) {
416 | const params = {
417 | session_id: state.auth.session_id,
418 | collection_id: id
419 | }
420 |
421 | if (state.media[id]) return Promise.resolve()
422 |
423 | return new Promise(async (resolve, reject) => {
424 | try {
425 | const resp = await api({route: 'info', params})
426 | if (resp.data.error) throw resp
427 |
428 | const data = resp.data.data
429 | commit('ADD_COLLECTION', data)
430 | resolve()
431 | } catch (err) {
432 | handleError(err, reject)
433 | }
434 | })
435 | },
436 |
437 | getMediaInfo ({commit, state, dispatch}, id) {
438 | const params = {
439 | session_id: state.auth.session_id,
440 | media_id: id,
441 | fields: MEDIA_FIELDS
442 | }
443 |
444 | if (state.media[id]) return Promise.resolve()
445 |
446 | return new Promise(async (resolve, reject) => {
447 | try {
448 | const resp = await api({route: 'info', params})
449 | if (resp.data.error) throw resp
450 |
451 | const data = resp.data.data
452 | commit('ADD_MEDIA', data)
453 | resolve()
454 | } catch (err) {
455 | handleError(err, reject)
456 | }
457 | })
458 | },
459 |
460 | leaveRoom ({state, commit}) {
461 | const roomRef = Firebase.getRef(`/rooms/${state.roomId}`)
462 | const usersRef = Firebase.getRef(`/roomUsers/${state.roomId}`)
463 | const connectedRef = Firebase.getRef(`/roomUsers/${state.roomId}/${Firebase.app.auth().currentUser.uid}`)
464 |
465 | roomRef.off()
466 | usersRef.off()
467 | connectedRef.remove()
468 |
469 | commit('UPDATE_ROOM', '')
470 | commit('UPDATE_CONNECTED', false)
471 | commit('UPDATE_CONNECTED_COUNT', 1)
472 | },
473 |
474 | enterRoom ({state, commit}, {id = uuid()}) {
475 | commit('UPDATE_ROOM', id)
476 |
477 | return new Promise(async (resolve, reject) => {
478 | try {
479 | await Firebase.init()
480 | const roomRef = Firebase.getRef(`/rooms/${id}`)
481 | const usersRef = Firebase.getRef(`/roomUsers/${id}`)
482 | const connectedRef = Firebase.getRef(`/roomUsers/${id}/${Firebase.app.auth().currentUser.uid}`)
483 | await connectedRef.set(true)
484 | connectedRef.onDisconnect().remove()
485 |
486 | const room = await roomRef.once('value')
487 | const value = room.val() || {
488 | playing: false,
489 | syncedTime: 0,
490 | host: Firebase.app.auth().currentUser.uid,
491 | hostOnly: false,
492 | route: {
493 | path: state.route.path,
494 | name: state.route.name
495 | }
496 | }
497 |
498 | if (!room.val()) {
499 | await roomRef.set(value)
500 | }
501 |
502 | roomRef.on('value', (snapshot) => {
503 | commit('UPDATE_ROOM_DATA', snapshot.val())
504 | })
505 |
506 | commit('UPDATE_CONNECTED', true)
507 | usersRef.on('value', (snapshot) => {
508 | if (snapshot.exists()) {
509 | commit('UPDATE_CONNECTED_COUNT', Object.keys(snapshot.val()).length)
510 | }
511 | })
512 |
513 | resolve(value)
514 | } catch (err) {
515 | reject(err)
516 | }
517 | })
518 | },
519 |
520 | updateRoomData ({state, getters}, obj) {
521 | if (state.roomData.hostOnly && !getters.isRoomHost) return Promise.resolve()
522 |
523 | return new Promise(async (resolve, reject) => {
524 | try {
525 | const roomRef = Firebase.getRef(`/rooms/${state.roomId}`)
526 |
527 | await roomRef.update(obj)
528 | resolve()
529 | } catch (err) {
530 | reject(err)
531 | }
532 | })
533 | },
534 |
535 | flashGuestMessage ({state, commit}) {
536 | if (state.guestMessage) return
537 |
538 | commit('UPDATE_GUEST_MESSAGE', true)
539 | setTimeout(() => {
540 | commit('UPDATE_GUEST_MESSAGE', false)
541 | }, 5000)
542 | }
543 | },
544 |
545 | mutations: {
546 | UPDATE_AUTH (state, obj) {
547 | const updated = Object.assign({}, state.auth, obj)
548 | localStorage.setItem('auth', JSON.stringify(updated))
549 | Vue.set(state, 'auth', updated)
550 | },
551 |
552 | UPDATE_LOCALES (state, arr) {
553 | state.locales = arr
554 | },
555 |
556 | REMOVE_AUTH (state) {
557 | localStorage.removeItem('auth')
558 | Vue.set(state, 'auth', {})
559 | },
560 |
561 | UPDATE_MAL (state, obj) {
562 | const updated = Object.assign({}, state.malAuth, obj)
563 | localStorage.setItem('malAuth', JSON.stringify(updated))
564 | Vue.set(state, 'malAuth', updated)
565 | },
566 |
567 | REMOVE_MAL_AUTH (state) {
568 | localStorage.removeItem('malAuth')
569 | Vue.set(state, 'malAuth', {})
570 | },
571 |
572 | UPDATE_AL (state, obj) {
573 | const updated = Object.assign({}, state.alAuth, obj)
574 | localStorage.setItem('alAuth', JSON.stringify(updated))
575 | Vue.set(state, 'alAuth', updated)
576 | },
577 |
578 | REMOVE_AL_AUTH (state) {
579 | localStorage.removeItem('alAuth')
580 | Vue.set(state, 'alAuth', {})
581 | },
582 |
583 | SET_SEARCH_IDS (state, arr) {
584 | state.searchIds = arr
585 | },
586 |
587 | SET_SEARCH_QUERY (state, str) {
588 | state.searchQuery = str
589 | },
590 |
591 | SET_QUEUE_DATA (state, arr) {
592 | Vue.set(state, 'queueData', arr)
593 | },
594 |
595 | SET_INITIAL_HISTORY (state, arr) {
596 | Vue.set(state, 'initialHistory', arr)
597 | },
598 |
599 | SET_RECENT (state, arr) {
600 | Vue.set(state, 'recent', arr)
601 | },
602 |
603 | ADD_SERIES (state, obj) {
604 | if (obj && obj.series_id) Vue.set(state.series, obj.series_id, obj)
605 | },
606 |
607 | ADD_SERIES_COLLECTION (state, {id, arr}) {
608 | if (arr) Vue.set(state.seriesCollections, id, arr)
609 | },
610 |
611 | ADD_COLLECTION (state, obj) {
612 | if (obj && obj.collection_id) Vue.set(state.collections, obj.collection_id, obj)
613 | },
614 |
615 | ADD_COLLECTION_MEDIA (state, {id, arr}) {
616 | if (arr) Vue.set(state.collectionMedia, id, arr)
617 | },
618 |
619 | ADD_MEDIA (state, obj) {
620 | if (obj && obj.media_id) Vue.set(state.media, obj.media_id, obj)
621 | },
622 |
623 | UPDATE_ROOM (state, str) {
624 | state.roomId = str
625 | },
626 |
627 | UPDATE_ROOM_DATA (state, obj) {
628 | Vue.set(state, 'roomData', obj)
629 | },
630 |
631 | UPDATE_CONNECTED (state, bool) {
632 | state.roomConnected = bool
633 | },
634 |
635 | UPDATE_CONNECTED_COUNT (state, int) {
636 | state.connectedCount = int
637 | },
638 |
639 | UPDATE_ROOM_MENU (state, bool) {
640 | state.roomMenu = bool
641 | },
642 |
643 | UPDATE_SERIES_QUEUE (state, {id, queueStatus}) {
644 | Vue.set(state.series[id], 'in_queue', queueStatus)
645 | },
646 |
647 | UPDATE_LIGHTS (state, bool) {
648 | state.lights = bool
649 | },
650 |
651 | SET_ERROR (state, bool) {
652 | state.error = bool
653 | },
654 |
655 | SET_EXPIRED_SESSION (state, str) {
656 | state.expiredSession = str
657 | },
658 |
659 | UPDATE_GUEST_MESSAGE (state, bool) {
660 | state.guestMessage = bool
661 | },
662 |
663 | SET_READ_EXTENSION (state) {
664 | localStorage.setItem('readExtension', 'true')
665 | state.readExtension = true
666 | }
667 | },
668 |
669 | getters: {
670 | isRoomHost (state) {
671 | return state.roomConnected && state.roomData.host === Firebase.app.auth().currentUser.uid
672 | }
673 | }
674 | })
675 |
676 | export default store
677 |
--------------------------------------------------------------------------------
/static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/remixz/umi/188084473e496026f5a996c401bd2599d6476580/static/.gitkeep
--------------------------------------------------------------------------------