diff --git a/.env b/.env new file mode 100644 index 00000000..e6a7afe5 --- /dev/null +++ b/.env @@ -0,0 +1,165 @@ +# Development configuration can be loaded form this file via env variables +# +# To enable flags, create a .env file: +# ``` +# cp .env.example .env +# ``` +# and set the values in the new .env file to what you prefer + + + +# ============================================================================ +# Config: +# +# This set of configurations allow you to change the configuration of existing +# features. +# ============================================================================ + + + +# Download Location +# +# This configuration sets the folder where the downloads should be saved to. + +CONFIG_DOWNLOAD_LOCATION=/tmp/popcorn-time-desktop + + + +# Persist Donwloads +# +# Determine if downloaded content should not be deleted after usage. If this +# is set to false, files will be persist to the download location. + +CONFIG_PERSIST_DOWNLOADS=false + + + +# Cache Timeout +# +# Determine how long the API cache should be held. This is measured in hours. +# This includes the cache for Torrent Providers and Metadata Providers. + +CONFIG_CACHE_TIMEOUT=1 + + + +# API Timeout +# +# Set the time, in milliseconds, that the API has to return. Increasing this +# may return faster torrents, but the API will take longer to find them. + +CONFIG_API_TIMEOUT=10000 + + + +# Maximum Connections +# +# This sets the maximum number of network connections per torrent. This limits +# the bandwidth consumption so other apps can continue to function. + +CONFIG_MAX_CONNECTIONS=20 + + + +# ============================================================================ +# Flags: +# +# These allow you to enable experimental features or non-recommended in +# PopcornTime with the flip of a switch! +# ============================================================================ + + + +# English subtitles support +# +# This flag allows subtitles in torrents to be played along with the movie. Be +# careful, since this is a very early stage experiment! Currently this only +# allows English subtitles. Support for subtitles of multiple languages is +# planned for a future release. + +FLAG_SUBTITLES=false + + + +# Allow torrents that do not have a quality. +# +# Butter validates and filters torrents based on a number of factors. Sometimes +# torrents with a large seed count fail are filtered because their quality cannot +# be determined. This flag allows those torrents to be verified and will most +# likely increase the seed count of torrents at the cost of those torrents being +# unverified. + +FLAG_UNVERIFIED_TORRENTS=false + + + +# Allow 'complete season' torrents +# +# 'Complete Seasons' is an experiment that significantly increases the amount +# of seeders of torrents. In many cases, 'Complete Show' torrents offer +# significantly more torrents than the traditional 'Shows' method. +# 'Complete Seasons' works by querying a complete season of a show and downloading +# only the wanted episode. While this can increase torrent counts, it increases +# the time to fetch torrents. + +FLAG_SEASON_COMPLETE=true + + + +# Enable experimental casting support +# +# This feature allows casting to a external devices. Note that this will only +# work if you have a global installation of node. Also only chromecast +# devices are supported at the moment 😢 but support for Airplay, DLNA, and +# upup protocols is planned! + +FLAG_CASTING=false + + + +# Allow video whose subtitles are rendered as part of the movie. +# +# This makes removing them impossible. If you can tolerate these kinds of movies +# enable them and your will likely increase your seeder count. + +FLAG_SUBTITLE_EMBEDDED_MOVIES=false + + + +# Show multiple video qualities +# +# Ideally, PopcornTime would be able to select the best torrent for the user, as +# opposed to having them manually chose. Manually choosing allows users to pick +# which qualify (ex. 1080p, 720p, etc) of torrent they want. + +FLAG_MANUAL_TORRENT_SELECTION=true + + + +# Only play torrents that are natively supported +# +# Filter torrents that are not natively supported. This will drastically reduce +# the seeder count of tv shows, since most tv shows are in non-native formats +# (mkv, avi, and others). Use this only if your OS is not supported by +# PopcornTime yet. + +FLAG_NATIVE_PLAYBACK_FILTERING=false + + + +# ============================================================================ +# Testing: +# +# This set of configuration is used for testing purposes only. It lets the +# compiler how to compile the app for the testing environment. +# ============================================================================ + + + +# Use Mock Data +# +# Force the API to use mock data. This method falls back to the network if no +# mock data is found. Mock data is located in ./tests/api/ and is named as +# *.mock.js. + +API_USE_MOCK_DATA=false diff --git a/.eslintrc b/.eslintrc index 66175d7b..9bbdbceb 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,7 +1,10 @@ { "extends": [ "airbnb", - "bliss" + "bliss", + "prettier", + "prettier/flowtype", + "prettier/react" ], "parser": "babel-eslint", "env": { @@ -10,6 +13,7 @@ }, "rules": { "class-methods-use-this": "off", + "flowtype/boolean-style": ["error", "boolean"], "flowtype/no-weak-types": "error", "fp/no-arguments": "off", "fp/no-class": "off", @@ -34,7 +38,10 @@ "no-plusplus": "off", "no-use-before-define": "off", "no-console": "off", - "promise/avoid-new": "off" + "promise/avoid-new": "off", + "react/sort-comp": ["error", { + "order": ["type-annotations", "static-methods", "lifecycle", "everything-else", "render"] + }] }, "settings": { "import/extensions": [".jsx", ".js"], diff --git a/app/actions/homePageActions.js b/app/actions/homePageActions.js index 4f10d80f..c90783ef 100644 --- a/app/actions/homePageActions.js +++ b/app/actions/homePageActions.js @@ -1,5 +1,7 @@ // @flow -export function setActiveMode(activeMode: string, activeModeOptions: Object = {}) { +import type { activeModeOptionsType, itemType } from '../components/home/Home.jsx'; + +export function setActiveMode(activeMode: string, activeModeOptions?: activeModeOptionsType = {}) { return { type: 'SET_ACTIVE_MODE', activeMode, @@ -7,7 +9,7 @@ export function setActiveMode(activeMode: string, activeModeOptions: Object = {} }; } -export function paginate(items: Array) { +export function paginate(items: Array) { return { type: 'PAGINATE', items @@ -26,7 +28,7 @@ export function clearAllItems() { }; } -export function setLoading(isLoading: bool) { +export function setLoading(isLoading: boolean) { return { type: 'SET_LOADING', isLoading diff --git a/app/api/Butter.js b/app/api/Butter.js index fd445b5e..b076174d 100644 --- a/app/api/Butter.js +++ b/app/api/Butter.js @@ -5,9 +5,7 @@ import TorrentAdapter from './torrents/TorrentAdapter'; import MetadataAdapter from './metadata/MetadataAdapter'; - export default class Butter { - getMovies(page: number = 1, limit: number = 50) { return MetadataAdapter.getMovies(page, limit); } @@ -46,10 +44,12 @@ export default class Butter { * @param {object} extendedDetails | Additional details provided for heuristics * @param {boolean} returnAll */ - getTorrent(imdbId: string, + getTorrent( + imdbId: string, type: string, - extendedDetails: Object = {}, - returnAll: bool = false) { + extendedDetails: { [option: string]: string | number } = {}, + returnAll: boolean = false + ) { return TorrentAdapter(imdbId, type, extendedDetails, returnAll); } @@ -57,7 +57,12 @@ export default class Butter { return MetadataAdapter.search(query, page); } - getSubtitles(imdbId: string, filename: string, length: number, metadata: Object) { + getSubtitles( + imdbId: string, + filename: string, + length: number, + metadata: Object + ) { return MetadataAdapter.getSubtitles(imdbId, filename, length, metadata); } diff --git a/app/api/Player.js b/app/api/Player.js index df325ad4..e5962e36 100644 --- a/app/api/Player.js +++ b/app/api/Player.js @@ -4,11 +4,9 @@ import plyr from 'plyr'; import childProcess from 'child_process'; import vlcCommand from 'vlc-command'; - const { powerSaveBlocker } = remote; export default class Player { - currentPlayer = 'plyr'; powerSaveBlockerId: number; @@ -18,7 +16,15 @@ export default class Player { */ player: plyr; - static nativePlaybackFormats = ['mp4', 'ogg', 'mov', 'webmv', 'mkv', 'wmv', 'avi']; + static nativePlaybackFormats = [ + 'mp4', + 'ogg', + 'mov', + 'webmv', + 'mkv', + 'wmv', + 'avi' + ]; static experimentalPlaybackFormats = []; @@ -41,13 +47,16 @@ export default class Player { this.player.restart(); } - static isFormatSupported(filename: string, mimeTypes: Array): bool { - return !!mimeTypes.find( - mimeType => filename.toLowerCase().includes(mimeType) + static isFormatSupported( + filename: string, + mimeTypes: Array + ): boolean { + return !!mimeTypes.find(mimeType => + filename.toLowerCase().includes(mimeType) ); } - initPlyr(streamingUrl: string, metadata: Object = {}): plyr { + initPlyr(streamingUrl: string, metadata = {}): plyr { console.info('Initializing plyr...'); this.currentPlayer = 'plyr'; this.powerSaveBlockerId = powerSaveBlocker.start('prevent-app-suspension'); @@ -62,10 +71,12 @@ export default class Player { player.source({ type: 'video', - sources: [{ - src: streamingUrl, - type: 'video/mp4' - }], + sources: [ + { + src: streamingUrl, + type: 'video/mp4' + } + ], ...metadata }); @@ -90,7 +101,9 @@ export default class Player { }); } - this.powerSaveBlockerId = powerSaveBlocker.start('prevent-app-suspension'); + this.powerSaveBlockerId = powerSaveBlocker.start( + 'prevent-app-suspension' + ); return true; }); diff --git a/app/api/Subtitle.js b/app/api/Subtitle.js index 3c91701b..866b3d35 100644 --- a/app/api/Subtitle.js +++ b/app/api/Subtitle.js @@ -6,9 +6,18 @@ import fs from 'fs'; import srt2vtt from 'srt2vtt'; import rndm from 'rndm'; - export const basePath = os.tmpdir(); -export const port = process.env.SUBTITLES_PORT || 4000; +export const port = typeof process.env.SUBTITLES_PORT === 'number' + ? parseInt(process.env.SUBTITLES_PORT, 10) + : 4000; + +export type subtitleType = { + filename: string, + basePath: string, + port: number, + fullPath: string, + buffer: Buffer +}; /** * Serve the file through http @@ -27,13 +36,13 @@ export function closeServer(server: express): express { return server.close(); } -export function convertFromBuffer(srtBuffer: Buffer): Promise { +export function convertFromBuffer(srtBuffer: Buffer): Promise { const randomString = rndm(16); const filename = `${randomString}.vtt`; const fullPath = path.join(basePath, filename); - return new Promise((resolve: Function, reject: Function) => { - srt2vtt(srtBuffer, (error: Error, vttBuffer: Buffer) => { + return new Promise((resolve, reject) => { + srt2vtt(srtBuffer, (error?: Error, vttBuffer: Buffer) => { if (error) reject(error); fs.writeFile(fullPath, vttBuffer, () => { diff --git a/app/api/Torrent.js b/app/api/Torrent.js index b289bf6f..4121b6d5 100644 --- a/app/api/Torrent.js +++ b/app/api/Torrent.js @@ -6,14 +6,12 @@ import os from 'os'; import WebTorrent from 'webtorrent'; import { isExactEpisode } from './torrents/BaseTorrentProvider'; - const port = 9090; export default class Torrent { + inProgress: boolean = false; - inProgress: bool = false; - - finished: bool = false; + finished: boolean = false; checkDownloadInterval: number; @@ -23,15 +21,20 @@ export default class Torrent { server: Object; - start(magnetURI: string, metadata: Object, supportedFormats: Array, cb) { + start( + magnetURI: string, + metadata: Object, + supportedFormats: Array, + cb + ) { if (this.inProgress) { throw new Error('Torrent already in progress'); } const { season, episode, activeMode } = metadata; const maxConns = process.env.CONFIG_MAX_CONNECTIONS - ? parseInt(process.env.CONFIG_MAX_CONNECTIONS, 10) - : 20; + ? parseInt(process.env.CONFIG_MAX_CONNECTIONS, 10) + : 20; this.engine = new WebTorrent({ maxConns }); this.inProgress = true; @@ -40,7 +43,9 @@ export default class Torrent { const cacheLocation = ((): string => { switch (process.env.CONFIG_PERSIST_DOWNLOADS) { case 'true': - return process.env.CONFIG_DOWNLOAD_LOCATION || '/tmp/popcorn-time-desktop'; + return ( + process.env.CONFIG_DOWNLOAD_LOCATION || '/tmp/popcorn-time-desktop' + ); default: return os.tmpdir(); } @@ -51,41 +56,49 @@ export default class Torrent { server.listen(port); this.server = server; - const { file, torrentIndex } = torrent.files.reduce((previous, current, index) => { - const formatIsSupported = !!supportedFormats.find( - format => current.name.includes(format) - ); + const { file, torrentIndex } = torrent.files.reduce( + (previous, current, index) => { + const formatIsSupported = !!supportedFormats.find(format => + current.name.includes(format) + ); - switch (activeMode) { - // Check if the current file is the exact episode we're looking for - case 'season_complete': - if (formatIsSupported && isExactEpisode(current.name, season, episode)) { - previous.file.deselect(); - return { - file: current, - torrentIndex: index - }; - } - - return previous; - - // Check if the current file is greater than the previous file - default: - if (formatIsSupported && current.length > previous.file.length) { - previous.file.deselect(); - return { - file: current, - torrentIndex: index - }; - } - - return previous; - } - }, { file: torrent.files[0], torrentIndex: 0 }); + switch (activeMode) { + // Check if the current file is the exact episode we're looking for + case 'season_complete': + if ( + formatIsSupported && + isExactEpisode(current.name, season, episode) + ) { + previous.file.deselect(); + return { + file: current, + torrentIndex: index + }; + } + + return previous; + + // Check if the current file is greater than the previous file + default: + if (formatIsSupported && current.length > previous.file.length) { + previous.file.deselect(); + return { + file: current, + torrentIndex: index + }; + } + + return previous; + } + }, + { file: torrent.files[0], torrentIndex: 0 } + ); if (typeof torrentIndex !== 'number') { console.warn('File List', torrent.files.map(_file => _file.name)); - throw new Error(`No torrent could be selected. Torrent Index: ${torrentIndex}`); + throw new Error( + `No torrent could be selected. Torrent Index: ${torrentIndex}` + ); } const buffer = 1 * 1024 * 1024; // 1MB @@ -139,11 +152,25 @@ export default class Torrent { } } -export function formatSpeeds({ downloadSpeed, - uploadSpeed, - progress, - numPeers, - ratio }: Object): Object { +type torrentSpeedsType = { + downloadSpeed: number, + uploadSpeed: number, + progress: number, + numPeers: number, + ratio: number +}; + +export function formatSpeeds( + torrentSpeeds: torrentSpeedsType +): torrentSpeedsType { + const { + downloadSpeed, + uploadSpeed, + progress, + numPeers, + ratio + } = torrentSpeeds; + return { downloadSpeed: downloadSpeed / 1000000, uploadSpeed: uploadSpeed / 1000000, @@ -156,22 +183,28 @@ export function formatSpeeds({ downloadSpeed, /** * Get the subtitle file buffer given an array of files */ -export function selectSubtitleFile(files: Array = [], +export function selectSubtitleFile( + files: Array<{ name: string }> = [], activeMode: string, - metadata: Object = {}): Object | bool { - return files.find(file => { - const formatIsSupported = file.name.includes('.srt'); - - switch (activeMode) { - // Check if the current file is the exact episode we're looking for - case 'season_complete': { - const { season, episode } = metadata; - return (formatIsSupported && isExactEpisode(file.name, season, episode)); - } + metadata: { season: number, episode: number } +): { name: string } | boolean { + return ( + files.find(file => { + const formatIsSupported = file.name.includes('.srt'); + + switch (activeMode) { + // Check if the current file is the exact episode we're looking for + case 'season_complete': { + const { season, episode } = metadata; + return ( + formatIsSupported && isExactEpisode(file.name, season, episode) + ); + } - // Check if the current file is greater than the previous file - default: - return formatIsSupported; - } - }) || false; + // Check if the current file is greater than the previous file + default: + return formatIsSupported; + } + }) || false + ); } diff --git a/app/api/metadata/MetadataAdapter.js b/app/api/metadata/MetadataAdapter.js index ff3c5fa0..9aec6740 100644 --- a/app/api/metadata/MetadataAdapter.js +++ b/app/api/metadata/MetadataAdapter.js @@ -3,24 +3,20 @@ * @flow */ import OpenSubtitles from 'opensubtitles-api'; -import { - merge, - resolveCache, - setCache -} from '../torrents/BaseTorrentProvider'; +import { merge, resolveCache, setCache } from '../torrents/BaseTorrentProvider'; import TraktMetadataProvider from './TraktMetadataProvider'; -import type { runtimeType } from './MetadataInterface'; - +import type { runtimeType } from './MetadataProviderInterface'; type subtitlesType = { kind: 'captions', label: string, srclang: string, src: string, - default: bool + default: boolean }; -const subtitlesEndpoint = 'https://popcorn-time-api-server.herokuapp.com/subtitles'; +const subtitlesEndpoint = + 'https://popcorn-time-api-server.herokuapp.com/subtitles'; const openSubtitles = new OpenSubtitles({ useragent: 'OSTestUserAgent', @@ -30,12 +26,10 @@ const openSubtitles = new OpenSubtitles({ }); function MetadataAdapter() { - return [ - new TraktMetadataProvider() - ]; + return [new TraktMetadataProvider()]; } -async function handleRequest(method: string, args: Array) { +async function interceptAndHandleRequest(method: string, args: Array) { const key = JSON.stringify(method) + JSON.stringify(args); if (resolveCache(key)) { @@ -43,8 +37,7 @@ async function handleRequest(method: string, args: Array) { } const results = await Promise.all( - MetadataAdapter() - .map(provider => provider[method].apply(provider, args)) // eslint-disable-line + MetadataAdapter().map(provider => provider[method].apply(provider, args)) // eslint-disable-line ); const mergedResults = merge(results); @@ -62,7 +55,7 @@ async function handleRequest(method: string, args: Array) { * @param {string} sortBy */ function search(...args: Array) { - return handleRequest('search', args); + return interceptAndHandleRequest('search', args); } /** @@ -71,7 +64,7 @@ function search(...args: Array) { * @param {string} imdbId */ function getMovie(...args: Array) { - return handleRequest('getMovie', args); + return interceptAndHandleRequest('getMovie', args); } /** @@ -83,7 +76,7 @@ function getMovie(...args: Array) { * @param {string} sortBy */ function getMovies(...args: Array) { - return handleRequest('getMovies', args); + return interceptAndHandleRequest('getMovies', args); } /** @@ -94,7 +87,7 @@ function getMovies(...args: Array) { * @param {number} limit | movie or show */ function getSimilar(...args: Array) { - return handleRequest('getSimilar', args); + return interceptAndHandleRequest('getSimilar', args); } /** @@ -105,7 +98,7 @@ function getSimilar(...args: Array) { * @param {number} limit | movie or show */ function getSeason(...args: Array) { - return handleRequest('getSeason', args); + return interceptAndHandleRequest('getSeason', args); } /** @@ -116,7 +109,7 @@ function getSeason(...args: Array) { * @param {number} limit | movie or show */ function getSeasons(...args: Array) { - return handleRequest('getSeasons', args); + return interceptAndHandleRequest('getSeasons', args); } /** @@ -127,7 +120,7 @@ function getSeasons(...args: Array) { * @param {number} limit | movie or show */ function getEpisode(...args: Array) { - return handleRequest('getEpisode', args); + return interceptAndHandleRequest('getEpisode', args); } /** @@ -138,7 +131,7 @@ function getEpisode(...args: Array) { * @param {number} limit | movie or show */ function getShow(...args: Array) { - return handleRequest('getShow', args); + return interceptAndHandleRequest('getShow', args); } /** @@ -149,7 +142,7 @@ function getShow(...args: Array) { * @param {number} limit | movie or show */ function getShows(...args: Array) { - return handleRequest('getShows', args); + return interceptAndHandleRequest('getShows', args); } /** @@ -169,8 +162,8 @@ async function getSubtitles( const defaultOptions = { sublanguageid: 'eng', - // sublanguageid: 'all', // @TODO - // hash: '8e245d9679d31e12', // @TODO + // sublanguageid: 'all', // @TODO + // hash: '8e245d9679d31e12', // @TODO filesize: length || undefined, filename: filename || undefined, season: metadata.season || undefined, @@ -194,9 +187,7 @@ async function getSubtitles( })(); return subtitles.then(res => - Object - .values(res) - .map(subtitle => formatSubtitle(subtitle)) + Object.values(res).map(subtitle => formatSubtitle(subtitle)) ); } @@ -208,7 +199,7 @@ async function getSubtitles( * @param {object} metadata | 'id', Required only remove */ function favorites(...args: Array) { - return handleRequest('favorites', args); + return interceptAndHandleRequest('favorites', args); } /** @@ -219,7 +210,7 @@ function favorites(...args: Array) { * @param {object} metadata | 'id', Required only remove */ function watchList(...args: Array) { - return handleRequest('watchList', args); + return interceptAndHandleRequest('watchList', args); } /** @@ -230,7 +221,7 @@ function watchList(...args: Array) { * @param {object} metadata | 'id', Required only remove */ function recentlyWatched(...args) { - return handleRequest('recentlyWatched', args); + return interceptAndHandleRequest('recentlyWatched', args); } /** @@ -245,8 +236,10 @@ export function convertRuntimeToHours(runtimeInMinutes: number): runtimeType { return { full: hours > 0 - ? `${hours} ${hours > 1 ? 'hours' : 'hour'}${minutes > 0 ? ` ${minutes} minutes` : ''}` - : `${minutes} minutes`, + ? `${hours} ${hours > 1 ? 'hours' : 'hour'}${minutes > 0 + ? ` ${minutes} minutes` + : ''}` + : `${minutes} minutes`, hours, minutes }; diff --git a/app/api/metadata/MetadataInterface.js b/app/api/metadata/MetadataProviderInterface.js similarity index 67% rename from app/api/metadata/MetadataInterface.js rename to app/api/metadata/MetadataProviderInterface.js index 78f850fb..3774d183 100644 --- a/app/api/metadata/MetadataInterface.js +++ b/app/api/metadata/MetadataProviderInterface.js @@ -9,48 +9,56 @@ type seasonType = { images: { full: string, medium: string, - thumb: string, + thumb: string } }; export type runtimeType = { - full: number | 'n/a', - hours: number | 'n/a', - minutes: number | 'n/a' + full: string, + hours: number, + minutes: number }; +export type certificationType = 'G' | 'PG' | 'PG-13' | 'R'; + +export type imagesType = { + fanart: { + full: string, + medium: string, + thumb: string + }, + poster: { + full: string, + medium: string, + thumb: string + } +} + export type contentType = { title: string, year: number, imdbId: string, id: string, type: 'movies' | 'shows', - certification: string, + certification: certificationType, summary: string, genres: Array, rating: number | 'n/a', runtime: runtimeType, trailer: string | 'n/a', - images: { - fanart: { - full: string, - medium: string, - thumb: string - }, - poster: { - full: string, - medium: string, - thumb: string - } - } + images: imagesType }; -export interface MetadataInterface { +export interface MetadataProviderInterface { getMovies: (page: number, limit: number) => Promise, getMovie: (imdbId: string) => contentType, getShows: (page: number, limit: number) => Promise, getShow: (imdbId: string) => contentType, - getSimilar: (type: string, imdbId: string, limit: number) => Promise>, + getSimilar: ( + type: string, + imdbId: string, + limit: number + ) => Promise>, getSeasons: (imdbId: string) => Promise>, getSeason: (imdbId: string, season: number) => Promise, @@ -61,5 +69,5 @@ export interface MetadataInterface { updateConfig: (type: string, method: string, metadata: contentType) => void, favorites: () => void, recentlyWatched: () => void, - watchList: () => void, + watchList: () => void } diff --git a/app/api/metadata/TraktMetadataProvider.js b/app/api/metadata/TraktMetadataProvider.js index d8a3f5e5..02b25a68 100644 --- a/app/api/metadata/TraktMetadataProvider.js +++ b/app/api/metadata/TraktMetadataProvider.js @@ -3,11 +3,12 @@ import fetch from 'isomorphic-fetch'; import Trakt from 'trakt.tv'; import { set, get } from '../../utils/Config'; import { convertRuntimeToHours } from './MetadataAdapter'; -import type { MetadataInterface, contentType } from './MetadataInterface'; - - -export default class TraktMetadataAdapter implements MetadataInterface { +import type { + MetadataProviderInterface, + contentType +} from './MetadataProviderInterface'; +export default class TraktMetadataAdapter implements MetadataProviderInterface { clientId = '647c69e4ed1ad13393bf6edd9d8f9fb6fe9faf405b44320a6b71ab960b4540a2'; clientSecret = 'f55b0a53c63af683588b47f6de94226b7572a6f83f40bd44c58a7c83fe1f2cb1'; @@ -22,74 +23,83 @@ export default class TraktMetadataAdapter implements MetadataInterface { } getMovies(page: number = 1, limit: number = 50) { - return this.trakt.movies.popular({ - paginate: true, - page, - limit, - extended: 'full,images,metadata' - }) + return this.trakt.movies + .popular({ + paginate: true, + page, + limit, + extended: 'full,images,metadata' + }) .then(movies => movies.map(movie => formatMetadata(movie, 'movies'))); } getMovie(imdbId: string) { - return this.trakt.movies.summary({ - id: imdbId, - extended: 'full,images,metadata' - }) + return this.trakt.movies + .summary({ + id: imdbId, + extended: 'full,images,metadata' + }) .then(movie => formatMetadata(movie, 'movies')); } getShows(page: number = 1, limit: number = 50) { - return this.trakt.shows.popular({ - paginate: true, - page, - limit, - extended: 'full,images,metadata' - }) + return this.trakt.shows + .popular({ + paginate: true, + page, + limit, + extended: 'full,images,metadata' + }) .then(shows => shows.map(show => formatMetadata(show, 'shows'))); } getShow(imdbId: string) { - return this.trakt.shows.summary({ - id: imdbId, - extended: 'full,images,metadata' - }) + return this.trakt.shows + .summary({ + id: imdbId, + extended: 'full,images,metadata' + }) .then(show => formatMetadata(show, 'shows')); } getSeasons(imdbId: string) { - return this.trakt.seasons.summary({ - id: imdbId, - extended: 'full,images,metadata' - }) - .then(res => res.filter(season => season.aired_episodes !== 0).map(season => ({ - season: season.number + 1, - overview: season.overview, - id: season.ids.imdb, - images: { - full: season.images.poster.full, - medium: season.images.poster.medium, - thumb: season.images.poster.thumb - } - }))); + return this.trakt.seasons + .summary({ + id: imdbId, + extended: 'full,images,metadata' + }) + .then(res => + res.filter(season => season.aired_episodes !== 0).map(season => ({ + season: season.number + 1, + overview: season.overview, + id: season.ids.imdb, + images: { + full: season.images.poster.full, + medium: season.images.poster.medium, + thumb: season.images.poster.thumb + } + })) + ); } getSeason(imdbId: string, season: number) { - return this.trakt.seasons.season({ - id: imdbId, - season, - extended: 'full,images,metadata' - }) + return this.trakt.seasons + .season({ + id: imdbId, + season, + extended: 'full,images,metadata' + }) .then(episodes => episodes.map(episode => formatSeason(episode))); } getEpisode(imdbId: string, season: number, episode: number) { - return this.trakt.episodes.summary({ - id: imdbId, - season, - episode, - extended: 'full,images,metadata' - }) + return this.trakt.episodes + .summary({ + id: imdbId, + season, + episode, + extended: 'full,images,metadata' + }) .then(res => formatSeason(res)); } @@ -111,11 +121,12 @@ export default class TraktMetadataAdapter implements MetadataInterface { * @param {string} imdbId | movie or show */ getSimilar(type: string = 'movies', imdbId: string, limit: number = 5) { - return this.trakt[type].related({ - id: imdbId, - limit, - extended: 'full,images,metadata' - }) + return this.trakt[type] + .related({ + id: imdbId, + limit, + extended: 'full,images,metadata' + }) .then(movies => movies.map(movie => formatMetadata(movie, type))); } @@ -134,7 +145,9 @@ export default class TraktMetadataAdapter implements MetadataInterface { case 'get': return get(property); case 'remove': { - const items = [...(get(property) || []).filter(item => item.id !== metadata.id)]; + const items = [ + ...(get(property) || []).filter(item => item.id !== metadata.id) + ]; return set(property, items); } default: @@ -193,15 +206,15 @@ function formatMovieSearch(movie) { id: movie.imdbID, type: movie.Type.includes('movie') ? 'movies' : 'shows', certification: movie.Rated, - summary: 'n/a', // omdbapi does not support + summary: 'n/a', // omdbapi does not support genres: [], - rating: 'n/a', // omdbapi does not support + rating: 'n/a', // omdbapi does not support runtime: { - full: 'n/a', // omdbapi does not support - hours: 'n/a', // omdbapi does not support + full: 'n/a', // omdbapi does not support + hours: 'n/a', // omdbapi does not support minutes: 'n/a' // omdbapi does not support }, - trailer: 'n/a', // omdbapi does not support + trailer: 'n/a', // omdbapi does not support images: { fanart: { full: movie.Poster || '', @@ -236,4 +249,3 @@ function formatSeason(season, image: string = 'screenshot') { function roundRating(rating: number): number { return Math.round(rating * 10) / 10; } - diff --git a/app/api/players/Cast.js b/app/api/players/Cast.js index b6d395a4..92e75bbc 100644 --- a/app/api/players/Cast.js +++ b/app/api/players/Cast.js @@ -2,7 +2,6 @@ const network = require('network-address'); const { argv } = require('yargs'); const cast = require('chromecast-player'); - const { url, title, image } = argv; const addr = url.replace('localhost', network()); @@ -11,8 +10,6 @@ cast({ type: 'video/mp4', metadata: { title, - images: [ - { url: image } - ] + images: [{ url: image }] } }); diff --git a/app/api/players/PlayerAdapter.js b/app/api/players/PlayerAdapter.js index 5d2bd3ed..fb9974bc 100644 --- a/app/api/players/PlayerAdapter.js +++ b/app/api/players/PlayerAdapter.js @@ -23,7 +23,7 @@ export interface PlayerAdapterInterface { supportedFormats: Array, - supportsSubtitles: bool, + supportsSubtitles: boolean, svgIconFilename: string } diff --git a/app/api/players/WebChimeraProvider.js b/app/api/players/WebChimeraProvider.js deleted file mode 100644 index e69de29b..00000000 diff --git a/app/api/torrents/BaseTorrentProvider.js b/app/api/torrents/BaseTorrentProvider.js index d58a4a0e..da53bd14 100644 --- a/app/api/torrents/BaseTorrentProvider.js +++ b/app/api/torrents/BaseTorrentProvider.js @@ -2,31 +2,34 @@ /* eslint prefer-template: 0 */ import cache from 'lru-cache'; import URL from 'url'; - +import type { torrentType } from './TorrentProviderInterface'; export const providerCache = cache({ maxAge: process.env.CONFIG_CACHE_TIMEOUT - ? parseInt(process.env.CONFIG_CACHE_TIMEOUT, 10) * 1000 * 60 * 60 - : 1000 * 60 * 60 // 1 hr + ? parseInt(process.env.CONFIG_CACHE_TIMEOUT, 10) * 1000 * 60 * 60 + : 1000 * 60 * 60 // 1 hr }); /** * Handle a promise and set a timeout */ -export function timeout(promise: Promise, time: number = 10000): Promise { +export function timeout( + promise: Promise, + time: number = 10000 +): Promise { return new Promise((resolve, reject) => { promise.then(res => resolve(res)).catch(err => console.log(err)); setTimeout(() => { reject(new Error('Timeout exceeded')); - }, process.env.CONFIG_API_TIMEOUT - ? parseInt(process.env.CONFIG_API_TIMEOUT, 10) - : time - ); + }, process.env.CONFIG_API_TIMEOUT ? parseInt(process.env.CONFIG_API_TIMEOUT, 10) : time); }); } -export function determineQuality(magnet: string, metadata: string = ''): string { +export function determineQuality( + magnet: string, + metadata: string = '' +): string { const lowerCaseMetadata = (metadata || magnet).toLowerCase(); if (process.env.FLAG_UNVERIFIED_TORRENTS === 'true') { @@ -40,9 +43,7 @@ export function determineQuality(magnet: string, metadata: string = ''): string // Filter videos with 'rendered' subtitles if (hasSubtitles(lowerCaseMetadata)) { - return process.env.FLAG_SUBTITLE_EMBEDDED_MOVIES === 'true' - ? '480p' - : ''; + return process.env.FLAG_SUBTITLE_EMBEDDED_MOVIES === 'true' ? '480p' : ''; } // Most accurate categorization @@ -64,8 +65,8 @@ export function determineQuality(magnet: string, metadata: string = ''): string if (hasNonNativeCodec(lowerCaseMetadata)) { return process.env.FLAG_SUPPORTED_PLAYBACK_FILTERING === 'true' - ? '720p' - : ''; + ? '720p' + : ''; } if (process.env.NODE_ENV === 'development') { @@ -75,26 +76,42 @@ export function determineQuality(magnet: string, metadata: string = ''): string return ''; } -export function formatSeasonEpisodeToString(season: number, episode: number): string { +export function formatSeasonEpisodeToString( + season: number, + episode: number +): string { return ( - ('s' + (String(season).length === 1 ? '0' + String(season) : String(season))) + - ('e' + (String(episode).length === 1 ? '0' + String(episode) : String(episode))) + 's' + + (String(season).length === 1 ? '0' + String(season) : String(season)) + + ('e' + + (String(episode).length === 1 ? '0' + String(episode) : String(episode))) ); } -export function formatSeasonEpisodeToObject(season: number, episode: ?number): Object { +export function formatSeasonEpisodeToObject( + season: number, + episode: ?number +): Object { return { - season: (String(season).length === 1 ? '0' + String(season) : String(season)), - episode: (String(episode).length === 1 ? '0' + String(episode) : String(episode)) + season: String(season).length === 1 ? '0' + String(season) : String(season), + episode: String(episode).length === 1 + ? '0' + String(episode) + : String(episode) }; } -export function isExactEpisode(title: string, season: number, episode: number): bool { - return title.toLowerCase().includes(formatSeasonEpisodeToString(season, episode)); +export function isExactEpisode( + title: string, + season: number, + episode: number +): boolean { + return title + .toLowerCase() + .includes(formatSeasonEpisodeToString(season, episode)); } export function getHealth(seeders: number, leechers: number = 0): string { - const ratio = (seeders && !!leechers) ? (seeders / leechers) : seeders; + const ratio = seeders && !!leechers ? seeders / leechers : seeders; if (seeders < 50) { return 'poor'; @@ -111,7 +128,7 @@ export function getHealth(seeders: number, leechers: number = 0): string { return 'poor'; } -export function hasNonEnglishLanguage(metadata: string): bool { +export function hasNonEnglishLanguage(metadata: string): boolean { if (metadata.includes('french')) return true; if (metadata.includes('german')) return true; if (metadata.includes('greek')) return true; @@ -129,38 +146,41 @@ export function hasNonEnglishLanguage(metadata: string): bool { return false; } -export function hasSubtitles(metadata: string): bool { +export function hasSubtitles(metadata: string): boolean { return metadata.includes('sub'); } -export function hasNonNativeCodec(metadata: string): bool { - return ( - metadata.includes('avi') || - metadata.includes('mkv') - ); +export function hasNonNativeCodec(metadata: string): boolean { + return metadata.includes('avi') || metadata.includes('mkv'); } export function sortTorrentsBySeeders(torrents: Array): Array { - return torrents.sort((prev: Object, next: Object) => ( - prev.seeders === next.seeders - ? 0 - : prev.seeders > next.seeders ? -1 : 1 - )); + return torrents.sort( + (prev: Object, next: Object) => + prev.seeders === next.seeders ? 0 : prev.seeders > next.seeders ? -1 : 1 + ); } -export function constructMovieQueries(title: string, imdbId: string): Array { +export function constructMovieQueries( + title: string, + imdbId: string +): Array { const queries = [ title, // default imdbId ]; return title.includes("'") - ? [...queries, title.replace(/'/g,'')] // eslint-disable-line - : queries; + ? [...queries, title.replace(/'/g, '')] // eslint-disable-line + : queries; } -export function constructSeasonQueries(title: string, season: number): Array { - const formattedSeasonNumber = `s${formatSeasonEpisodeToObject(season, 1).season}`; +export function constructSeasonQueries( + title: string, + season: number +): Array { + const formattedSeasonNumber = `s${formatSeasonEpisodeToObject(season, 1) + .season}`; return [ `${title} season ${season}`, @@ -191,16 +211,15 @@ export function resolveEndpoint(defaultEndpoint: string, providerId: string) { } } -export function getIdealTorrent(torrents: Array): Object { +export function getIdealTorrent(torrents: Array): torrentType { const idealTorrent = torrents .filter(torrent => !!torrent) - .filter(torrent => - !!torrent && - !!torrent.magnet && - typeof torrent.seeders === 'number' + .filter( + torrent => + !!torrent && !!torrent.magnet && typeof torrent.seeders === 'number' ); - return idealTorrent.sort((prev: Object, next: Object) => { + return idealTorrent.sort((prev: torrentType, next: torrentType) => { if (prev.seeders === next.seeders) { return 0; } @@ -215,16 +234,16 @@ export function handleProviderError(error: Error) { } } -export function resolveCache(key: string): bool | Object { +export function resolveCache(key: string): boolean | any { if (process.env.API_USE_MOCK_DATA === 'true') { const mock = { ...require('../../../test/api/metadata.mock'), // eslint-disable-line global-require - ...require('../../../test/api/torrent.mock') // eslint-disable-line global-require + ...require('../../../test/api/torrent.mock') // eslint-disable-line global-require }; const resolvedCacheItem = Object.keys(mock).find( - (mockKey: string): bool => key.includes(`${mockKey}"`) && - !!Object.keys(mock[mockKey]).length + (mockKey: string): boolean => + key.includes(`${mockKey}"`) && !!Object.keys(mock[mockKey]).length ); if (resolvedCacheItem) { @@ -236,19 +255,12 @@ export function resolveCache(key: string): bool | Object { return false; } - return ( - providerCache.has(key) - ? providerCache.get(key) - : false - ); + return providerCache.has(key) ? providerCache.get(key) : false; } export function setCache(key: string, value: any) { if (process.env.NODE_ENV === 'development') { console.info('Setting cache key:', key); } - return providerCache.set( - key, - value - ); + return providerCache.set(key, value); } diff --git a/app/api/torrents/ExtraTorrentProvider.js b/app/api/torrents/ExtraTorrentProvider.js deleted file mode 100644 index e0db69ec..00000000 --- a/app/api/torrents/ExtraTorrentProvider.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * @TODO - * @flow - */ -import fetch from 'isomorphic-fetch'; -import cheerio from 'cheerio'; -import { handleProviderError } from './BaseTorrentProvider'; -import type { ProviderInterface } from './ProviderInterface'; - - -const extratorrentUrl = 'http://extratorrent.cc'; - -export default class ExtraTorrent implements ProviderInterface { - - fetch(searchQuery: string) { - return fetch( - `${extratorrentUrl}/search/?search=${encodeURIComponent(searchQuery)}&new=1&x=0&y=0` - ) - .then(res => res.text()) - .then(torrent => this.format(torrent)) - .catch(error => { - handleProviderError(error); - return []; - }); - } - - format(torrentsText: string) { - const $ = cheerio.load(torrentsText); - - if ($('.tl').find('tr').length > 3) { - $('.tl tr').map(torrent => { - if ($(torrent).find('td a').attr('href') !== '#') { - const findTorrentLink = $(torrent).find('td a'); - - const magnet = extratorrentUrl + - findTorrentLink.attr('href') - .split('torrent_download') - .join('download'); - - const title = findTorrentLink.attr('title') - .split('Download ') - .join('') - .split(' torrent') - .join(''); - const size = $(torrent).find('td') - .next() - .next() - .next() - .first() - .text(); - const seeders = $(torrent).find('td.sy').text(); - const leechers = $(torrent).find('td.ly').text(); - - return { - title, - seeders, - leechers, - size, - magnet - }; - } - - return {}; - }); - } - } -} diff --git a/app/api/torrents/KatTorrentProvider.js b/app/api/torrents/KatTorrentProvider.js index 711b61bd..d79d62ee 100644 --- a/app/api/torrents/KatTorrentProvider.js +++ b/app/api/torrents/KatTorrentProvider.js @@ -9,15 +9,13 @@ import { handleProviderError, resolveEndpoint } from './BaseTorrentProvider'; -import type { ProviderInterface } from './ProviderInterface'; - +import type { TorrentProviderInterface } from './TorrentProviderInterface'; const endpoint = 'https://katproxy.al'; const providerId = 'KAT'; const resolvedEndpoint = resolveEndpoint(endpoint, providerId); -export default class KatTorrentProvider implements ProviderInterface { - +export default class KatTorrentProvider implements TorrentProviderInterface { static providerName = 'Kat'; static fetch(query: string) { @@ -33,12 +31,13 @@ export default class KatTorrentProvider implements ProviderInterface { }); } - static formatTorrent(torrent: Object) { + static formatTorrent(torrent) { return { magnet: torrent.magnet, seeders: torrent.seeders, leechers: torrent.leechers, - metadata: String(torrent.title + torrent.magnet) || String(torrent.magnet), + metadata: + String(torrent.title + torrent.magnet) || String(torrent.magnet), _provider: 'kat' }; } @@ -47,52 +46,48 @@ export default class KatTorrentProvider implements ProviderInterface { return fetch(resolvedEndpoint).then(res => res.ok).catch(() => false); } - static provide(imdbId: string, type: string, extendedDetails: Object = {}) { + static provide(imdbId: string, type: string, extendedDetails = {}) { const { searchQuery } = extendedDetails; switch (type) { case 'movies': - return timeout( - Promise.all( - constructMovieQueries(searchQuery, imdbId).map(query => this.fetch(query)) - ) - ) - // Flatten array of arrays to an array with no empty arrays - .then( - res => merge(res).filter(array => array.length !== 0) + return ( + timeout( + Promise.all( + constructMovieQueries(searchQuery, imdbId).map(query => + this.fetch(query) + ) + ) ) - .catch(error => { - handleProviderError(error); - return []; - }); + // Flatten array of arrays to an array with no empty arrays + .then(res => merge(res).filter(array => array.length !== 0)) + .catch(error => { + handleProviderError(error); + return []; + }) + ); case 'shows': { const { season, episode } = extendedDetails; return this.fetch( `${searchQuery} ${formatSeasonEpisodeToString(season, episode)}` - ) - .catch(error => { - handleProviderError(error); - return []; - }); + ).catch(error => { + handleProviderError(error); + return []; + }); } case 'season_complete': { const { season } = extendedDetails; const queries = constructSeasonQueries(searchQuery, season); - return timeout( - Promise.all( - queries.map(query => this.fetch(query)) - ) - ) - .then( - res => res.reduce((previous, current) => ( - previous.length && current.length - ? [...previous, ...current] - : previous.length && !current.length - ? previous - : current - )) + return timeout(Promise.all(queries.map(query => this.fetch(query)))) + .then(res => + res.reduce( + (previous, current) => + previous.length && current.length + ? [...previous, ...current] + : previous.length && !current.length ? previous : current + ) ) .catch(error => { handleProviderError(error); diff --git a/app/api/torrents/PbTorrentProvider.js b/app/api/torrents/PbTorrentProvider.js index a9e01baa..e40dd668 100644 --- a/app/api/torrents/PbTorrentProvider.js +++ b/app/api/torrents/PbTorrentProvider.js @@ -12,97 +12,91 @@ import { handleProviderError, resolveEndpoint } from './BaseTorrentProvider'; -import type { ProviderInterface } from './ProviderInterface'; - +import type { TorrentProviderInterface } from './TorrentProviderInterface'; const endpoint = 'https://pirate-bay-endpoint.herokuapp.com'; const providerId = 'PB'; const resolvedEndpoint = resolveEndpoint(endpoint, providerId); -export default class PbTorrentProvider implements ProviderInterface { - +export default class PbTorrentProvider implements TorrentProviderInterface { static providerName = 'PirateBay'; - static fetch(searchQuery: string): Promise> { + static fetch(searchQuery: string) { // HACK: Temporary solution to improve performance by side stepping // PirateBay's database errors. const searchQueryUrl = `${resolvedEndpoint}/search/${searchQuery}`; - return timeout( - fetch(searchQueryUrl) - ) + return timeout(fetch(searchQueryUrl)) .then(res => res.json()) - .then(torrents => torrents.map( - torrent => this.formatTorrent(torrent) - )) + .then(torrents => torrents.map(torrent => this.formatTorrent(torrent))) .catch(error => { handleProviderError(error); return []; }); } - static formatTorrent(torrent: Object): Object { + static formatTorrent(torrent) { return { magnet: torrent.magnetLink, seeders: parseInt(torrent.seeders, 10), leechers: parseInt(torrent.leechers, 10), - metadata: (String(torrent.name) || '') + - (String(torrent.magnetLink) || '') + - (String(torrent.link) || ''), + metadata: + (String(torrent.name) || '') + + (String(torrent.magnetLink) || '') + + (String(torrent.link) || ''), _provider: 'pb' }; } - static getStatus(): Promise { + static getStatus(): Promise { return fetch(resolvedEndpoint).then(res => res.ok).catch(() => false); } - static provide(imdbId: string, type: string, extendedDetails: Object = {}) { + static provide(imdbId: string, type: string, extendedDetails = {}) { if (!extendedDetails.searchQuery) { - return new Promise((resolve) => resolve([])); + return new Promise(resolve => resolve([])); } const { searchQuery } = extendedDetails; switch (type) { case 'movies': { - return Promise.all( - constructMovieQueries(searchQuery, imdbId).map(query => this.fetch(query)) - ) - // Flatten array of arrays to an array with no empty arrays - .then( - res => merge(res).filter(array => array.length !== 0) + return ( + Promise.all( + constructMovieQueries(searchQuery, imdbId).map(query => + this.fetch(query) + ) ) - .catch(error => { - handleProviderError(error); - return []; - }); + // Flatten array of arrays to an array with no empty arrays + .then(res => merge(res).filter(array => array.length !== 0)) + .catch(error => { + handleProviderError(error); + return []; + }) + ); } case 'shows': { const { season, episode } = extendedDetails; return this.fetch( `${searchQuery} ${formatSeasonEpisodeToString(season, episode)}` - ) - .catch(error => { - handleProviderError(error); - return []; - }); + ).catch(error => { + handleProviderError(error); + return []; + }); } case 'season_complete': { const { season } = extendedDetails; const queries = constructSeasonQueries(searchQuery, season); - return Promise.all( - queries.map(query => this.fetch(query)) - ) - // Flatten array of arrays to an array with no empty arrays - .then( - res => merge(res).filter(array => array.length !== 0) - ) - .catch(error => { - handleProviderError(error); - return []; - }); + return ( + Promise.all(queries.map(query => this.fetch(query))) + // Flatten array of arrays to an array with no empty arrays + .then(res => merge(res).filter(array => array.length !== 0)) + .catch(error => { + handleProviderError(error); + return []; + }) + ); } default: return []; diff --git a/app/api/torrents/PctTorrentProvider.js b/app/api/torrents/PctTorrentProvider.js index b54b615e..40ba95ca 100644 --- a/app/api/torrents/PctTorrentProvider.js +++ b/app/api/torrents/PctTorrentProvider.js @@ -5,15 +5,13 @@ import { timeout, resolveEndpoint } from './BaseTorrentProvider'; -import type { ProviderInterface } from './ProviderInterface'; - +import type { TorrentProviderInterface } from './TorrentProviderInterface'; const endpoint = 'http://api-fetch.website/tv'; const providerId = 'PCT'; const resolvedEndpoint = resolveEndpoint(endpoint, providerId); -export default class PctTorrentProvider implements ProviderInterface { - +export default class PctTorrentProvider implements TorrentProviderInterface { static providerName = 'PopcornTime API'; static shows = {}; @@ -21,8 +19,9 @@ export default class PctTorrentProvider implements ProviderInterface { static async fetch(imdbId: string, type: string, extendedDetails) { const urlTypeParam = type === 'movies' ? 'movie' : 'show'; const request = timeout( - fetch(`${resolvedEndpoint}/${urlTypeParam}/${imdbId}`) - .then(res => res.json()) + fetch(`${resolvedEndpoint}/${urlTypeParam}/${imdbId}`).then(res => + res.json() + ) ); switch (type) { @@ -31,14 +30,15 @@ export default class PctTorrentProvider implements ProviderInterface { [ { ...movie.torrents.en['1080p'], quality: '1080p' }, { ...movie.torrents.en['720p'], quality: '720p' } - ] - .map(torrent => this.formatMovieTorrent(torrent)) + ].map(torrent => this.formatMovieTorrent(torrent)) ); case 'shows': { const { season, episode } = extendedDetails; const show = await request - .then(res => res.episodes.map(eachEpisode => this.formatEpisode(eachEpisode))) + .then(res => + res.episodes.map(eachEpisode => this.formatEpisode(eachEpisode)) + ) .catch(error => { handleProviderError(error); return []; @@ -64,8 +64,8 @@ export default class PctTorrentProvider implements ProviderInterface { static filterTorrents(show, season: number, episode: number) { const filterTorrents = show .filter( - eachEpisode => eachEpisode.season === season && - eachEpisode.episode === episode + eachEpisode => + eachEpisode.season === season && eachEpisode.episode === episode ) .map(eachEpisode => eachEpisode.torrents); @@ -108,17 +108,15 @@ export default class PctTorrentProvider implements ProviderInterface { static provide(imdbId: string, type: string, extendedDetails = {}) { switch (type) { case 'movies': - return this.fetch(imdbId, type, extendedDetails) - .catch(error => { - handleProviderError(error); - return []; - }); + return this.fetch(imdbId, type, extendedDetails).catch(error => { + handleProviderError(error); + return []; + }); case 'shows': - return this.fetch(imdbId, type, extendedDetails) - .catch(error => { - handleProviderError(error); - return []; - }); + return this.fetch(imdbId, type, extendedDetails).catch(error => { + handleProviderError(error); + return []; + }); default: return []; } diff --git a/app/api/torrents/ProviderInterface.js b/app/api/torrents/ProviderInterface.js deleted file mode 100644 index 8a470d62..00000000 --- a/app/api/torrents/ProviderInterface.js +++ /dev/null @@ -1,15 +0,0 @@ -// @flow -type providerResponseType = { - quality: string, - magnet: string, - seeders: number, - leechers: number, - metadata: '', - _provider: 'string' -}; - -export interface ProviderInterface { - static getStatus: () => Promise, - static fetch: (imdbId: string) => Array, - static provide: (imdbId: string, type: string) => Array -} diff --git a/app/api/torrents/TorrentAdapter.js b/app/api/torrents/TorrentAdapter.js index 6868f0ad..287297ec 100644 --- a/app/api/torrents/TorrentAdapter.js +++ b/app/api/torrents/TorrentAdapter.js @@ -10,7 +10,9 @@ import { merge } from './BaseTorrentProvider'; - +/** + * @TODO: Use ES6 dynamic imports here + */ const providers = [ require('./YtsTorrentProvider'), require('./PbTorrentProvider'), @@ -23,9 +25,9 @@ export default async function TorrentAdapter( imdbId: string, type: string, extendedDetails, - returnAll: bool = false, + returnAll: boolean = false, method: string = 'all', - cache: bool = true + cache: boolean = true ) { const args = JSON.stringify({ extendedDetails, returnAll, method }); @@ -33,8 +35,8 @@ export default async function TorrentAdapter( return resolveCache(args); } - const torrentPromises = providers.map( - provider => provider.provide(imdbId, type, extendedDetails) + const torrentPromises = providers.map(provider => + provider.provide(imdbId, type, extendedDetails) ); switch (method) { @@ -45,7 +47,7 @@ export default async function TorrentAdapter( switch (type) { case 'movies': return selectTorrents( - appendAttributes(providerResults).map((result) => ({ + appendAttributes(providerResults).map(result => ({ ...result, method: 'movies' })), @@ -98,12 +100,12 @@ export default async function TorrentAdapter( * @return {array} */ function appendAttributes(providerResults) { - const formattedResults = merge(providerResults).map((result) => ({ + const formattedResults = merge(providerResults).map(result => ({ ...result, health: getHealth(result.seeders || 0, result.leechers || 0), quality: 'quality' in result - ? result.quality - : determineQuality(result.magnet, result.metadata, result) + ? result.quality + : determineQuality(result.magnet, result.metadata, result) })); return formattedResults; @@ -111,12 +113,9 @@ function appendAttributes(providerResults) { export function filterShows(show, season: number, episode: number) { return ( - show.metadata.toLowerCase().includes( - formatSeasonEpisodeToString( - season, - episode - ) - ) && + show.metadata + .toLowerCase() + .includes(formatSeasonEpisodeToString(season, episode)) && show.seeders !== 0 && show.magnet ); @@ -130,20 +129,22 @@ export function filterShowsComplete(show, season: number) { metadata.includes(`${season} [complete]`) || metadata.includes(`${season} - complete`) || metadata.includes(`season ${season}`) || - metadata.includes(`s${formatSeasonEpisodeToObject(season).season}`) && - !metadata.includes('e0') && - show.seeders !== 0 && - show.magnet + (metadata.includes(`s${formatSeasonEpisodeToObject(season).season}`) && + !metadata.includes('e0') && + show.seeders !== 0 && + show.magnet) ); } export function getStatuses() { - return Promise - .all(providers.map(provider => provider.getStatus())) - .then(providerStatuses => providerStatuses.map((status, index) => ({ + return Promise.all( + providers.map(provider => provider.getStatus()) + ).then(providerStatuses => + providerStatuses.map((status, index) => ({ providerName: providers[index].providerName, online: status - }))); + })) + ); } /** @@ -153,23 +154,23 @@ export function getStatuses() { export function selectTorrents( torrents, sortMethod: string = 'seeders', - returnAll: bool = false, - key: string) { + returnAll: boolean = false, + key: string +) { const sortedTorrents = sortTorrentsBySeeders( - torrents.filter((torrent) => - torrent.quality !== 'n/a' && - torrent.quality !== '' && - !!torrent.magnet + torrents.filter( + torrent => + torrent.quality !== 'n/a' && torrent.quality !== '' && !!torrent.magnet ) ); const formattedTorrents = returnAll ? sortedTorrents : { - '480p': sortedTorrents.find((torrent) => torrent.quality === '480p'), - '720p': sortedTorrents.find((torrent) => torrent.quality === '720p'), - '1080p': sortedTorrents.find((torrent) => torrent.quality === '1080p') - }; + '480p': sortedTorrents.find(torrent => torrent.quality === '480p'), + '720p': sortedTorrents.find(torrent => torrent.quality === '720p'), + '1080p': sortedTorrents.find(torrent => torrent.quality === '1080p') + }; setCache(key, formattedTorrents); diff --git a/app/api/torrents/TorrentProviderInterface.js b/app/api/torrents/TorrentProviderInterface.js new file mode 100644 index 00000000..feec815f --- /dev/null +++ b/app/api/torrents/TorrentProviderInterface.js @@ -0,0 +1,33 @@ +// @flow +export type fetchType = { + quality: string, + magnet: string, + seeders: number, + leechers: number, + metadata: '', + _provider: 'string' +}; + +export type torrentType = { + ...fetchType, + health: healthType, + quality: qualityType, + method: torrentQueryType +}; + +export type healthType = 'poor' | 'decent' | 'healthy'; + +export type torrentMethodType = 'all' | 'race'; + +export type qualityType = '1080p' | '720p' | '480p'; + +export type torrentQueryType = 'movies' | 'show' | 'season_complete'; + +export interface TorrentProviderInterface { + static getStatus: () => Promise, + static fetch: (imdbId: string) => Promise>, + static provide: ( + imdbId: string, + type: torrentType + ) => Promise> +} diff --git a/app/api/torrents/YtsTorrentProvider.js b/app/api/torrents/YtsTorrentProvider.js index bcbf3f12..d1cef273 100644 --- a/app/api/torrents/YtsTorrentProvider.js +++ b/app/api/torrents/YtsTorrentProvider.js @@ -5,54 +5,53 @@ import { timeout, resolveEndpoint } from './BaseTorrentProvider'; -import type { ProviderInterface } from './ProviderInterface'; - +import type { TorrentProviderInterface } from './TorrentProviderInterface'; const endpoint = 'https://yts.ag'; const providerId = 'YTS'; const resolvedEndpoint = resolveEndpoint(endpoint, providerId); -export default class YtsTorrentProvider implements ProviderInterface { - +export default class YtsTorrentProvider implements TorrentProviderInterface { static providerName = 'YTS'; - static fetch(imdbId: string): Promise { + static fetch(imdbId: string) { return timeout( - fetch([ - `${resolvedEndpoint}/api/v2/list_movies.json`, - `?query_term=${imdbId}`, - '&order_by=desc&sort_by=seeds&limit=50' - ].join('')) - ) - .then(res => res.json()); + fetch( + [ + `${resolvedEndpoint}/api/v2/list_movies.json`, + `?query_term=${imdbId}`, + '&order_by=desc&sort_by=seeds&limit=50' + ].join('') + ) + ).then(res => res.json()); } - static formatTorrent(torrent: Object): Object { + static formatTorrent(torrent) { return { quality: determineQuality(torrent.quality), magnet: constructMagnet(torrent.hash), seeders: parseInt(torrent.seeds, 10), leechers: parseInt(torrent.peers, 10), - metadata: (String(torrent.url) + String(torrent.hash)) || String(torrent.hash), + metadata: + String(torrent.url) + String(torrent.hash) || String(torrent.hash), _provider: 'yts' }; } - static getStatus(): Promise { + static getStatus(): Promise { return fetch('https://yts.ag/api/v2/list_movies.json') .then(res => !!res.ok) .catch(() => false); } - static provide(imdbId: string, type: string): Promise> { + static provide(imdbId, type) { switch (type) { case 'movies': - return this.fetch(imdbId) - .then(results => { - if (!results.data.movie_count) return []; - const torrents = results.data.movies[0].torrents; - return torrents.map(this.formatTorrent); - }); + return this.fetch(imdbId).then(results => { + if (!results.data.movie_count) return []; + const torrents = results.data.movies[0].torrents; + return torrents.map(this.formatTorrent); + }); default: return Promise.resolve([]); } diff --git a/app/components/card/Card.jsx b/app/components/card/Card.jsx index b1663332..ac048e6d 100644 --- a/app/components/card/Card.jsx +++ b/app/components/card/Card.jsx @@ -2,27 +2,36 @@ * Card in the CardList component * @flow */ -import React, { PropTypes } from 'react'; +import React from 'react'; import { Link } from 'react-router'; import Rating from './Rating.jsx'; +type Props = { + title: string, + starColor?: string, + image: string, + id: string, + rating: number | 'n/a', + type: string +}; + +export default function Card(props: Props) { + const { type, image, id, rating, title, starColor } = props; -export default function Card({ type, image, id, rating, title, starColor }) { const placeholder = process.env.NODE_ENV === 'production' ? './images/posterholder.png' : './app/images/posterholder.png'; const backgroundImageStyle = { - backgroundImage: `url(${image.toLowerCase() !== 'n/a' ? image : placeholder})` + backgroundImage: `url(${image.toLowerCase() !== 'n/a' + ? image + : placeholder})` }; return (
-
+
@@ -35,27 +44,12 @@ export default function Card({ type, image, id, rating, title, starColor }) { ? : null}
- {type === 'search' - ?
Kind: {type}
- : null} + {type === 'search' ?
Kind: {type}
: null}
); } -Card.propTypes = { - title: PropTypes.string.isRequired, - starColor: PropTypes.string, - image: PropTypes.string.isRequired, - id: PropTypes.string.isRequired, - genres: PropTypes.arrayOf(PropTypes.string).isRequired, - rating: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.string - ]).isRequired, - type: PropTypes.string.isRequired -}; - Card.defaultProps = { starColor: '#848484' }; diff --git a/app/components/card/CardList.jsx b/app/components/card/CardList.jsx index 08923055..9e46480e 100644 --- a/app/components/card/CardList.jsx +++ b/app/components/card/CardList.jsx @@ -2,18 +2,28 @@ * A list of thumbnail poster images of items that are rendered on the home page * @flow */ -import React, { PropTypes } from 'react'; +import React from 'react'; import Card from './Card.jsx'; import Loader from '../loader/Loader.jsx'; +import type { contentType } from '../../api/metadata/MetadataProviderInterface'; +type Props = { + title?: string, + limit?: number, + items: Array, + isLoading: boolean, + isFinished: boolean +}; + +export default function CardList(props: Props) { + const { items, isLoading, isFinished, title, limit } = props; -export default function CardList({ items, isLoading, isFinished, title, limit }) { return (
-

{title || ''}

+

{title}

- {(limit ? (items.filter((e, i) => (i < limit))) : items).map((item: Object) => ( + {(limit ? items.filter((e, i) => i < limit) : items).map(item => - ))} + )}
@@ -34,25 +44,9 @@ export default function CardList({ items, isLoading, isFinished, title, limit }) ); } -CardList.propTypes = { - title: PropTypes.string, - limit: PropTypes.number, - items: PropTypes.arrayOf(PropTypes.shape({ - title: PropTypes.string.isRequired, - id: PropTypes.string.isRequired, - year: PropTypes.number.isRequired, - type: PropTypes.string.isRequired, - rating: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.string - ]), - genres: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired - })).isRequired, - isLoading: PropTypes.bool.isRequired, - isFinished: PropTypes.bool.isRequired -}; - CardList.defaultProps = { + title: '', + limit: null, items: [], isLoading: false, isFinished: false, diff --git a/app/components/card/Rating.jsx b/app/components/card/Rating.jsx index 5b677b74..ab163d11 100644 --- a/app/components/card/Rating.jsx +++ b/app/components/card/Rating.jsx @@ -1,32 +1,29 @@ // @flow -import React, { PropTypes } from 'react'; +import React from 'react'; import StarRatingComponent from 'react-star-rating-component'; +type Props = { + rating: number | 'n/a', + starColor: string, + emptyStarColor?: string +}; -export default function RatingComponent({ rating, starColor, emptyStarColor }: Object) { +export default function RatingComponent(props: Props) { return ( } renderStarIcon={() => } name={'rating'} - starColor={starColor} - emptyStarColor={emptyStarColor} - value={rating / 2} + starColor={props.starColor} + emptyStarColor={props.emptyStarColor} + value={typeof props.rating === 'string' ? props.rating : props.rating / 2} editing={false} /> ); } -RatingComponent.propTypes = { - rating: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.string - ]), - starColor: PropTypes.string, - emptyStarColor: PropTypes.string -}; - RatingComponent.defaultProps = { rating: 0, - starColor: '#848484' + starColor: '#848484', + emptyStarColor: 'rgba(255, 255, 255, 0.2)' }; diff --git a/app/components/header/Header.jsx b/app/components/header/Header.jsx index af5abd35..c79b430e 100644 --- a/app/components/header/Header.jsx +++ b/app/components/header/Header.jsx @@ -1,18 +1,28 @@ // @flow /* eslint react/no-set-state: 0 */ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; import { browserHistory } from 'react-router'; import classNames from 'classnames'; import Butter from '../../api/Butter'; +type Props = { + setActiveMode: (mode: string, options?: { searchQuery: string }) => void, + activeMode: string +}; export default class Header extends Component { + props: Props; + + state: { + searchQuery: string + }; - constructor(props) { + butter: Butter; + + constructor(props: Props) { super(props); this.butter = new Butter(); - this.state = { searchQuery: '' }; @@ -25,13 +35,13 @@ export default class Header extends Component { this.props.setActiveMode('search', { searchQuery }); } - handleSearchChange(event: Object) { + handleSearchChange(event: SyntheticEvent) { this.setState({ searchQuery: event.target.value }); } - handleKeyPress(event: Object) { + handleKeyPress(event: SyntheticEvent) { if (event.key === 'Enter') { browserHistory.push('/search'); this.props.setActiveMode('search', { @@ -53,11 +63,8 @@ export default class Header extends Component { active: activeMode === 'movies' })} > - setActiveMode('movies')} - > - Movies (current) + setActiveMode('movies')}> + Movies (current)
  • - setActiveMode('shows')} - > - TV Shows - + setActiveMode('shows')}> + TV Shows +
  • @@ -93,8 +97,8 @@ export default class Header extends Component { @@ -102,8 +106,3 @@ export default class Header extends Component { ); } } - -Header.propTypes = { - setActiveMode: PropTypes.func.isRequired, - activeMode: PropTypes.string.isRequired -}; diff --git a/app/components/home/Home.jsx b/app/components/home/Home.jsx index 853abdc0..3360bb29 100644 --- a/app/components/home/Home.jsx +++ b/app/components/home/Home.jsx @@ -1,54 +1,76 @@ // @flow -import React, { Component, PropTypes } from 'react'; +/* eslint react/no-unused-prop-types: 0 */ +import React, { Component } from 'react'; import VisibilitySensor from 'react-visibility-sensor'; -// import { shell } from 'electron'; -// import notie from 'notie'; import Butter from '../../api/Butter'; import Header from '../header/Header.jsx'; import CardList from '../card/CardList.jsx'; -// import CheckUpdate from '../../utils/CheckUpdate'; - - -// HACK: This is a temporary way of checking running a check only once. There -// needs to be a better way of solving this. Ideally, it could be registered -// as a startup task. -// -// setTimeout(() => { -// requestIdleCallback(() => { -// CheckUpdate().then(res => -// (res === true -// ? notie.confirm('Update Available! 😁', 'Sure!', 'Nahh', () => { -// shell.openExternal( -// process.env.APP_DOWNLOAD_URL || -// 'https://github.com/amilajack/popcorn-time-desktop/releases' -// ); -// }) -// : console.info('Using latest semver! 😁')) -// ) -// .catch(res => console.log(res)); -// }); -// }, 3000); + +export type activeModeOptionsType = { + [option: string]: any +}; + +export type itemType = { + title: string, + id: string, + year: number, + type: string, + rating: number | 'n/a', + genres: Array +}; + +type Props = { + actions: { + setActiveMode: (mode: string, options: Object) => void, + paginate: ( + activeMode: string, + activeModeOptions?: activeModeOptionsType + ) => void, + clearAllItems: () => void, + setLoading: (isLoading: boolean) => void + }, + activeMode: string, + activeModeOptions: activeModeOptionsType, + modes: { + movies: { + page: number, + limit: number, + items: { + title: string, + id: string, + year: number, + type: string, + rating: number | 'n/a', + genres: Array + } + } + }, + items: Array, + isLoading: boolean, + infinitePagination: boolean +}; export default class Home extends Component { + props: Props; butter: Butter; - _didMount: bool; + didMount: boolean; onChange: () => void; - constructor(props: Object) { + constructor(props: Props) { super(props); this.butter = new Butter(); this.onChange = this.onChange.bind(this); } componentDidMount() { - this._didMount = true; + this.didMount = true; document.addEventListener('scroll', this.initInfinitePagination.bind(this)); } - componentWillReceiveProps(nextProps: Object) { + componentWillReceiveProps(nextProps: Props) { if ( JSON.stringify(nextProps.activeModeOptions) !== JSON.stringify(this.props.activeModeOptions) @@ -61,11 +83,14 @@ export default class Home extends Component { } componentWillUnmount() { - this._didMount = false; - document.removeEventListener('scroll', this.initInfinitePagination.bind(this)); + this.didMount = false; + document.removeEventListener( + 'scroll', + this.initInfinitePagination.bind(this) + ); } - async onChange(isVisible: bool) { + async onChange(isVisible: boolean) { if (isVisible && !this.props.isLoading) { await this.paginate(this.props.activeMode, this.props.activeModeOptions); } @@ -79,21 +104,21 @@ export default class Home extends Component { * @param {string} queryType | 'search', 'movies', 'shows', etc * @param {object} queryParams | { searchQuery: 'game of thrones' } */ - async paginate(queryType: string, activeModeOptions: Object = {}) { + async paginate( + queryType: string, + activeModeOptions: activeModeOptionsType = {} + ) { this.props.actions.setLoading(true); // HACK: This is a temporary solution. // Waiting on: https://github.com/yannickcr/eslint-plugin-react/issues/818 - /* eslint react/prop-types: 0 */ const { limit, page } = this.props.modes[queryType]; const items = await (async () => { switch (queryType) { case 'search': { - return this.butter.search( - activeModeOptions.searchQuery, page - ); + return this.butter.search(activeModeOptions.searchQuery, page); } case 'movies': return this.butter.getMovies(page, limit); @@ -115,7 +140,9 @@ export default class Home extends Component { */ initInfinitePagination() { if (this.props.infinitePagination) { - const scrollDimentions = document.querySelector('body').getBoundingClientRect(); + const scrollDimentions = document + .querySelector('body') + .getBoundingClientRect(); if (scrollDimentions.bottom < 2000 && !this.props.isLoading) { this.paginate(this.props.activeMode, this.props.activeModeOptions); } @@ -126,64 +153,12 @@ export default class Home extends Component { const { activeMode, actions, items, isLoading } = this.props; return (
    -
    +
    - - + +
    ); } } - -Home.propTypes = { - actions: PropTypes.shape({ - setActiveMode: PropTypes.func.isRequired, - paginate: PropTypes.func.isRequired, - clearAllItems: PropTypes.func.isRequired, - setLoading: PropTypes.func.isRequired, - setCurrentPlayer: PropTypes.func.isRequired - }).isRequired, - activeMode: PropTypes.string.isRequired, - activeModeOptions: PropTypes.shape({ - searchQuery: PropTypes.string - }).isRequired, - modes: PropTypes.shape({ - movies: PropTypes.shape({ - page: PropTypes.number.isRequired, - limit: PropTypes.number.isRequired, - items: PropTypes.arrayOf(PropTypes.shape({ - title: PropTypes.string.isRequired, - id: PropTypes.string.isRequired, - year: PropTypes.number.isRequired, - type: PropTypes.string.isRequired, - rating: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.string - ]), - genres: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired - }).isRequired) - }) - }).isRequired, - items: PropTypes.arrayOf(PropTypes.shape({ - title: PropTypes.string.isRequired, - id: PropTypes.string.isRequired, - year: PropTypes.number.isRequired, - type: PropTypes.string.isRequired, - rating: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.string - ]), - genres: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired - })).isRequired, - isLoading: PropTypes.bool.isRequired, - infinitePagination: PropTypes.bool.isRequired -}; diff --git a/app/components/item/Item.jsx b/app/components/item/Item.jsx index 82f41319..cd6e4030 100644 --- a/app/components/item/Item.jsx +++ b/app/components/item/Item.jsx @@ -2,7 +2,7 @@ * Movie component that is responsible for playing movies * @flow */ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; import { Dropdown, DropdownToggle, @@ -19,47 +19,98 @@ import Torrent from '../../api/Torrent'; import CardList from '../card/CardList.jsx'; import Rating from '../card/Rating.jsx'; import Show from '../show/Show.jsx'; -import { - convertFromBuffer, - startServer -} from '../../api/Subtitle'; +import { convertFromBuffer, startServer } from '../../api/Subtitle'; import Player from '../../api/Player'; - +import type { contentType, imagesType } from '../../api/metadata/MetadataProviderInterface'; +import type { + torrentType, + qualityType +} from '../../api/torrents/TorrentProviderInterface'; const SUMMARY_CHAR_LIMIT = 300; -export default class Movie extends Component { - - butter: Butter; +type playerType = 'Default' | 'plyr' | 'Chromecast'; + +type torrentSelectionType = { + default: torrentType, + [quality: qualityType]: + | torrentType + | { + quality?: string, + magnet?: string, + seeders: 0, + health?: string, + quality?: string + } +}; - torrent: Torrent; +type Props = { + itemId: string, + activeMode: string +}; - player: Player; +type State = { + item?: { + ...contentType, + images: ?imagesType + }, + similarItems: Array, + selectedSeason: number, + selectedEpisode: number, + seasons: [], + season: [], + episode: {}, + episodes: [], + currentPlayer: playerType, + playbackIsActive: boolean, + fetchingTorrents: boolean, + dropdownOpen: boolean, + idealTorrent: torrentType, + torrent: torrentSelectionType, + servingUrl: string, + similarLoading: boolean, + metadataLoading: boolean, + torrentInProgress: boolean, + torrentProgress: number, + isFinished: boolean +}; - toggle: Function; +export default class Item extends Component { + props: Props; - setPlayer: Function; + state: State; - stopPlayback: Function; + butter: Butter; - startPlayback: Function; + torrent: Torrent; - selectShow: Function; + player: Player; - defaultTorrent: Object = { - default: { quality: undefined, magnet: undefined, seeders: 0 }, - '1080p': { quality: undefined, magnet: undefined, seeders: 0 }, - '720p': { quality: undefined, magnet: undefined, seeders: 0 }, - '480p': { quality: undefined, magnet: undefined, seeders: 0 } + defaultTorrent: torrentSelectionType = { + default: { quality: undefined, magnet: undefined, health: undefined, method: undefined, seeders: 0 }, + '1080p': { quality: undefined, magnet: undefined, health: undefined, method: undefined, seeders: 0 }, + '720p': { quality: undefined, magnet: undefined, health: undefined, method: undefined, seeders: 0 }, + '480p': { quality: undefined, magnet: undefined, health: undefined, method: undefined, seeders: 0 } }; - initialState: Object = { + initialState: State = { item: { - images: { - fanart: {}, - poster: {} - }, - runtime: {} + id: '', + imdbId: '', + rating: 'n/a', + summary: '', + title: '', + trailer: '', + type: '', + year: 0, + certification: 'n/a', + genres: [], + images: null, + runtime: { + full: '', + hours: 0, + minutes: 0, + } }, selectedSeason: 1, selectedEpisode: 1, @@ -77,7 +128,7 @@ export default class Movie extends Component { torrentProgress: 0 }; - constructor(props: Object) { + constructor(props: Props) { super(props); this.butter = new Butter(); @@ -91,7 +142,7 @@ export default class Movie extends Component { /** * Check which players are available on the system */ - setPlayer(player: string) { + setPlayer(player: playerType) { this.setState({ currentPlayer: player }); } @@ -106,7 +157,8 @@ export default class Movie extends Component { this.stopPlayback(); this.player.destroy(); - this.setState({ // eslint-disable-line + this.setState({ + // eslint-disable-line ...this.initialState, dropdownOpen: false, currentPlayer: 'Default' @@ -118,7 +170,7 @@ export default class Movie extends Component { this.player.destroy(); } - componentWillReceiveProps(nextProps: Object) { + componentWillReceiveProps(nextProps: Props) { this.stopPlayback(); this.setState({ @@ -132,19 +184,28 @@ export default class Movie extends Component { this.setState(this.initialState, () => { if (this.props.activeMode === 'shows') { this.getShowData( - 'seasons', itemId, this.state.selectedSeason, this.state.selectedEpisode + 'seasons', + itemId, + this.state.selectedSeason, + this.state.selectedEpisode ); } }); return Promise.all([ - this.getItem(itemId) - .then((item: Object) => this.getTorrent(itemId, item.title, 1, 1)), + this.getItem(itemId).then((item: contentType) => + this.getTorrent(itemId, item.title, 1, 1) + ), this.getSimilar(itemId) ]); } - async getShowData(type: string, imdbId: string, season: number, episode: number) { + async getShowData( + type: string, + imdbId: string, + season?: number, + episode?: number + ) { switch (type) { case 'seasons': this.setState({ seasons: [], episodes: [], episode: {} }); @@ -155,6 +216,9 @@ export default class Movie extends Component { }); break; case 'episodes': + if (!season) { + throw new Error('"season" not provided to getShowData()'); + } this.setState({ episodes: [], episode: {} }); this.setState({ episodes: await this.butter.getSeason(imdbId, season), @@ -162,6 +226,9 @@ export default class Movie extends Component { }); break; case 'episode': + if (!season || !episode) { + throw new Error('"season" or "episode" not provided to getShowData()'); + } this.setState({ episode: {} }); this.setState({ episode: await this.butter.getEpisode(imdbId, season, episode) @@ -194,7 +261,12 @@ export default class Movie extends Component { return item; } - async getTorrent(imdbId: string, title: string, season: number, episode: number) { + async getTorrent( + imdbId: string, + title: string, + season: number, + episode: number + ) { this.setState({ fetchingTorrents: true, idealTorrent: this.defaultTorrent, @@ -205,9 +277,13 @@ export default class Movie extends Component { const { torrent, idealTorrent } = await (async () => { switch (this.props.activeMode) { case 'movies': { - const _torrent = await this.butter.getTorrent(imdbId, this.props.activeMode, { - searchQuery: title - }); + const _torrent = await this.butter.getTorrent( + imdbId, + this.props.activeMode, + { + searchQuery: title + } + ); return { torrent: _torrent, idealTorrent: getIdealTorrent([ @@ -233,9 +309,18 @@ export default class Movie extends Component { return { torrent: { - '1080p': getIdealTorrent([shows['1080p'], seasonComplete['1080p']]), - '720p': getIdealTorrent([shows['720p'], seasonComplete['720p']]), - '480p': getIdealTorrent([shows['480p'], seasonComplete['480p']]) + '1080p': getIdealTorrent([ + shows['1080p'], + seasonComplete['1080p'] + ]), + '720p': getIdealTorrent([ + shows['720p'], + seasonComplete['720p'] + ]), + '480p': getIdealTorrent([ + shows['480p'], + seasonComplete['480p'] + ]) }, idealTorrent: getIdealTorrent([ shows['1080p'], @@ -249,11 +334,15 @@ export default class Movie extends Component { } return { - torrent: await this.butter.getTorrent(imdbId, this.props.activeMode, { - season, - episode, - searchQuery: title - }), + torrent: await this.butter.getTorrent( + imdbId, + this.props.activeMode, + { + season, + episode, + searchQuery: title + } + ), idealTorrent: getIdealTorrent([ torrent['1080p'] || this.defaultTorrent, torrent['720p'] || this.defaultTorrent, @@ -288,7 +377,10 @@ export default class Movie extends Component { this.setState({ similarLoading: true }); try { - const similarItems = await this.butter.getSimilar(this.props.activeMode, imdbId); + const similarItems = await this.butter.getSimilar( + this.props.activeMode, + imdbId + ); this.setState({ similarItems, @@ -310,7 +402,11 @@ export default class Movie extends Component { } } - selectShow(type: string, selectedSeason: number, selectedEpisode: number = 1) { + selectShow( + type: string, + selectedSeason: number, + selectedEpisode: number = 1 + ) { switch (type) { case 'episodes': this.setState({ selectedSeason }); @@ -319,8 +415,18 @@ export default class Movie extends Component { break; case 'episode': this.setState({ selectedSeason, selectedEpisode }); - this.getShowData('episode', this.state.item.id, selectedSeason, selectedEpisode); - this.getTorrent(this.state.item.id, this.state.item.title, selectedSeason, selectedEpisode); + this.getShowData( + 'episode', + this.state.item.id, + selectedSeason, + selectedEpisode + ); + this.getTorrent( + this.state.item.id, + this.state.item.title, + selectedSeason, + selectedEpisode + ); break; default: throw new Error('Invalid selectShow() type'); @@ -334,7 +440,11 @@ export default class Movie extends Component { * 4. Serve the file through http * 5. Override the default subtitle retrieved from the API */ - async getSubtitles(subtitleTorrentFile: Object = {}, activeMode: string, item: Object) { + async getSubtitles( + subtitleTorrentFile: Object = {}, + activeMode: string, + item: contentType + ) { // Retrieve list of subtitles const subtitles = await this.butter.getSubtitles( item.imdbId, @@ -358,11 +468,12 @@ export default class Movie extends Component { }); // Override the default subtitle - const mergedResults = subtitles.map((subtitle: Object) => ( - subtitle.default === true - ? { ...subtitle, src: `http://localhost:${port}/${filename}` } - : subtitle - )); + const mergedResults = subtitles.map( + (subtitle: Object) => + subtitle.default === true + ? { ...subtitle, src: `http://localhost:${port}/${filename}` } + : subtitle + ); return mergedResults; } @@ -394,79 +505,103 @@ export default class Movie extends Component { ...Player.nativePlaybackFormats ]; - this.torrent.start(magnet, metadata, formats, async (servingUrl: string, - file: Object, - files: string, - torrent: string, - subtitle: string - ) => { - console.log(`serving at: ${servingUrl}`); - this.setState({ servingUrl }); - - const filename = file.name; - const subtitles = subtitle && process.env.FLAG_SUBTITLES === 'true' - ? await this.getSubtitles( - subtitle, - this.props.activeMode, - this.state.item - ) - : []; - - switch (this.state.currentPlayer) { - case 'VLC': - return this.player.initVLC(servingUrl); - case 'Chromecast': { - const { title } = this.state.item; - const { full } = this.state.item.images.fanart; - const command = [ - 'node ./.tmp/Cast.js', - `--url '${servingUrl}'`, - `--title '${title}'`, - `--image ${full}` - ].join(' '); - - return exec(command, (_error, stdout, stderr) => { - if (_error) { - return console.error(`Chromecast Exec Error: ${_error}`); - } - return [ - console.log(`stdout: ${stdout}`), - console.log(`stderr: ${stderr}`) - ]; - }); - } - case 'Default': - if (Player.isFormatSupported(filename, Player.nativePlaybackFormats)) { - this.player.initPlyr(servingUrl, { - poster: this.state.item.images.fanart.thumb, - tracks: subtitles + this.torrent.start( + magnet, + metadata, + formats, + async ( + servingUrl: string, + file: { name: string }, + files: string, + torrent: string, + subtitle: string + ) => { + console.log(`serving at: ${servingUrl}`); + this.setState({ servingUrl }); + + const filename = file.name; + const subtitles = subtitle && process.env.FLAG_SUBTITLES === 'true' + ? await this.getSubtitles( + subtitle, + this.props.activeMode, + this.state.item + ) + : []; + + switch (this.state.currentPlayer) { + case 'VLC': + return this.player.initVLC(servingUrl); + case 'Chromecast': { + const { title } = this.state.item; + const { full } = this.state.item.images.fanart; + const command = [ + 'node ./.tmp/Cast.js', + `--url '${servingUrl}'`, + `--title '${title}'`, + `--image ${full}` + ].join(' '); + + return exec(command, (_error, stdout, stderr) => { + if (_error) { + return console.error(`Chromecast Exec Error: ${_error}`); + } + return [ + console.log(`stdout: ${stdout}`), + console.log(`stderr: ${stderr}`) + ]; }); - this.toggleActive(); - } else if (Player.isFormatSupported(filename, [ - ...Player.nativePlaybackFormats, - ...Player.experimentalPlaybackFormats - ])) { - notie.alert(2, 'The format of this video is not playable', 2); - console.warn(`Format of filename ${filename} not supported`); - console.warn('Files retrieved:', files); } - break; - default: - console.error('Invalid player'); - break; - } + case 'Default': + if ( + Player.isFormatSupported(filename, Player.nativePlaybackFormats) + ) { + this.player.initPlyr(servingUrl, { + poster: this.state.item.images.fanart.thumb, + tracks: subtitles + }); + this.toggleActive(); + } else if ( + Player.isFormatSupported(filename, [ + ...Player.nativePlaybackFormats, + ...Player.experimentalPlaybackFormats + ]) + ) { + notie.alert(2, 'The format of this video is not playable', 2); + console.warn(`Format of filename ${filename} not supported`); + console.warn('Files retrieved:', files); + } + break; + default: + console.error('Invalid player'); + break; + } - return torrent; - }, downloaded => { - console.log('DOWNLOADING', downloaded); - }); + return torrent; + }, + downloaded => { + console.log('DOWNLOADING', downloaded); + } + ); } render() { const { - item, idealTorrent, torrent, servingUrl, torrentInProgress, - fetchingTorrents, dropdownOpen, currentPlayer, seasons, selectedSeason, - episodes, selectedEpisode, similarItems, similarLoading, isFinished, playbackIsActive + item, + idealTorrent, + torrent, + servingUrl, + torrentInProgress, + fetchingTorrents, + dropdownOpen, + currentPlayer, + seasons, + selectedSeason, + episodes, + selectedEpisode, + similarItems, + similarLoading, + isFinished, + playbackIsActive } = this.state; const { activeMode } = this.props; @@ -501,10 +636,7 @@ export default class Movie extends Component { })} > - @@ -514,10 +646,7 @@ export default class Movie extends Component {
    -
    +
    - {item.genres - ?
    {item.genres.join(', ')}
    - : null} + {item.genres ?
    {item.genres.join(', ')}
    : null}
    {/* HACK: Prefer a CSS solution to this, using text-overflow: ellipse */}
    {item.summary ? item.summary.length > SUMMARY_CHAR_LIMIT - ? `${item.summary.slice(0, SUMMARY_CHAR_LIMIT)}...` - : item.summary + ? `${item.summary.slice(0, SUMMARY_CHAR_LIMIT)}...` + : item.summary : ''}
    {item.rating ? + emptyStarColor={'rgba(255, 255, 255, 0.2)'} + starColor={'white'} + rating={item.rating} + /> : null}
    @@ -587,10 +714,8 @@ export default class Movie extends Component { {/* Torrent Selection */} - {activeMode === 'shows' ? : null} + {activeMode === 'shows' + ? + : null}
    - {items.map((item: Object, index: number) => ( - - - - ))} -
    - ); -} diff --git a/app/components/show/Show.jsx b/app/components/show/Show.jsx index 6c252fb1..21b6a140 100644 --- a/app/components/show/Show.jsx +++ b/app/components/show/Show.jsx @@ -1,24 +1,46 @@ // @flow -import React, { PropTypes } from 'react'; +import React from 'react'; import classNames from 'classnames'; +type Props = { + selectShow: (type: string, season: number, episode?: number) => void, + selectedSeason: number, + selectedEpisode: number, + seasons: Array<{ + season: number, + overview: string + }>, + episodes: Array<{ + episode: number, + overview: string, + title: string + }> +}; + +export default function Show(props: Props) { + const { + seasons, + selectShow, + selectedSeason, + episodes, + selectedEpisode + } = props; -export default function Show({ seasons, selectShow, selectedSeason, episodes, selectedEpisode }) { return (
    @@ -26,16 +48,17 @@ export default function Show({ seasons, selectShow, selectedSeason, episodes, se @@ -44,11 +67,9 @@ export default function Show({ seasons, selectShow, selectedSeason, episodes, se
  • Season overview:

  • - {seasons.length && - selectedSeason && - seasons[selectedSeason] - ? seasons[selectedSeason].overview - : null} + {seasons.length && selectedSeason && seasons[selectedSeason] + ? seasons[selectedSeason].overview + : null}
  • @@ -56,11 +77,9 @@ export default function Show({ seasons, selectShow, selectedSeason, episodes, se
  • Episode overview:

  • - {episodes.length && - selectedSeason && - episodes[selectedEpisode] - ? episodes[selectedEpisode].overview - : null} + {episodes.length && selectedSeason && episodes[selectedEpisode] + ? episodes[selectedEpisode].overview + : null}
  • @@ -68,19 +87,6 @@ export default function Show({ seasons, selectShow, selectedSeason, episodes, se ); } -Show.propTypes = { - selectShow: PropTypes.func.isRequired, - seasons: PropTypes.arrayOf(PropTypes.shape({ - - })).isRequired, - episodes: PropTypes.arrayOf(PropTypes.shape({ - episode: PropTypes.number.isRequired, - title: PropTypes.string.isRequired - })).isRequired, - selectedSeason: PropTypes.number.isRequired, - selectedEpisode: PropTypes.number -}; - Show.defaultProps = { seasons: [], episodes: [], diff --git a/app/containers/HomePage.jsx b/app/containers/HomePage.jsx index ecfd2306..800a6b2d 100644 --- a/app/containers/HomePage.jsx +++ b/app/containers/HomePage.jsx @@ -8,7 +8,6 @@ import { connect } from 'react-redux'; import * as HomeActions from '../actions/homePageActions'; import Home from '../components/home/Home.jsx'; - function mapStateToProps(state) { return { activeMode: state.homePageReducer.activeMode, diff --git a/app/containers/ItemPage.jsx b/app/containers/ItemPage.jsx index f537d09e..9b631ac4 100644 --- a/app/containers/ItemPage.jsx +++ b/app/containers/ItemPage.jsx @@ -1,26 +1,22 @@ // @flow -import React, { PropTypes } from 'react'; +import React from 'react'; import Item from '../components/item/Item.jsx'; +type Props = { + params: { + itemId: string, + activeMode: string + } +}; -export default function ItemPage({ params }) { +export default function ItemPage(props: Props) { return (
    - +
    ); } -ItemPage.propTypes = { - params: PropTypes.shape({ - itemId: PropTypes.string.isRequired, - activeMode: PropTypes.string.isRequired - }).isRequired -}; - ItemPage.defaultProps = { params: {} }; diff --git a/app/index.jsx b/app/index.jsx index b5217694..3fba8e0a 100644 --- a/app/index.jsx +++ b/app/index.jsx @@ -16,8 +16,8 @@ render( ); if (module.hot) { - module.hot.accept('./containers/Root', () => { - const NextRoot = require('./containers/Root'); // eslint-disable-line global-require + module.hot.accept('./containers/Root.jsx', () => { + const NextRoot = require('./containers/Root.jsx'); // eslint-disable-line global-require render( diff --git a/app/utils/CheckUpdate.js b/app/utils/CheckUpdate.js index 8a2a97ef..280f26cd 100644 --- a/app/utils/CheckUpdate.js +++ b/app/utils/CheckUpdate.js @@ -11,7 +11,7 @@ export const defaultUpdateEndpoint = /** * Return if the current application version is the latest */ -export default function CheckUpdate(): Promise { +export default function CheckUpdate(): Promise { const currentSemvar = remote.app.getVersion(); return fetch(defaultUpdateEndpoint) @@ -22,6 +22,6 @@ export default function CheckUpdate(): Promise { ).length); } -export function isNewerSemvar(current: string, next: string): bool { +export function isNewerSemvar(current: string, next: string): boolean { return semver.gt(current, next); } diff --git a/package.json b/package.json index 45476582..7e6cf7b3 100644 --- a/package.json +++ b/package.json @@ -151,19 +151,20 @@ "dotenv": "4.0.0", "download": "6.2.3", "electron": "1.6.11", - "electron-builder": "19.7.1", - "electron-debug": "1.1.0", + "electron-builder": "^19.8.0", + "electron-debug": "^1.2.0", "electron-devtools-installer": "2.2.0", "electron-rebuild": "^1.5.11", "electron-squirrel-startup": "1.0.0", "eslint": "3.19.0", "eslint-config-airbnb": "^15.0.1", "eslint-config-bliss": "^1.0.9", + "eslint-config-prettier": "^2.2.0", "eslint-formatter-pretty": "1.1.0", - "eslint-import-resolver-webpack": "0.8.1", + "eslint-import-resolver-webpack": "^0.8.3", "eslint-loader": "1.8.0", "eslint-nibble-ignore": "3.0.0", - "eslint-plugin-import": "2.3.0", + "eslint-plugin-import": "^2.6.0", "eslint-plugin-jest": "20.0.3", "eslint-plugin-jsx-a11y": "5.0.3", "eslint-plugin-promise": "3.5.0", diff --git a/postinstall.js b/postinstall.js index f11ad3e2..88c5c7df 100644 --- a/postinstall.js +++ b/postinstall.js @@ -9,21 +9,14 @@ import extract from 'extract-zip'; const version = process.env.PREBUILT_FFMPEG_RELEASE || '0.16.0'; const baseDir = path.join(__dirname, 'node_modules', 'electron', 'dist'); -function copy(filepath: string, dest: string): bool { - try { - fs.accessSync(path.join(__dirname, filepath)); - return true; - } catch (e) { - fs.writeFileSync( - path.join(__dirname, dest), - fs.readFileSync(path.join(__dirname, filepath)) - ); - - return true; - } +function copy(filepath: string, dest: string) { + fs.writeFileSync( + path.join(__dirname, dest), + fs.readFileSync(path.join(__dirname, filepath)) + ); } -function setupCasting(): bool { +function setupCasting(): boolean { const tmpPath = path.join(__dirname, 'app', 'dist', '.tmp'); mkdirp(tmpPath, err => { @@ -37,7 +30,7 @@ function setupCasting(): bool { return true; } -function addEnvFile(): bool { +function addEnvFileIfNotExist(): boolean { // Check if it exists try { fs.accessSync(path.join(__dirname, '.env')); @@ -82,7 +75,7 @@ function getUrl(): { platform: string, dest: string } { } } -function setupFFMPEG() { +function setupFfmpeg() { const { platform, dest } = getUrl(); const zipLocation = path.join( __dirname, @@ -100,5 +93,5 @@ function setupFFMPEG() { } setupCasting(); -setupFFMPEG(); -addEnvFile(); +setupFfmpeg(); +addEnvFileIfNotExist(); diff --git a/test/.eslintrc b/test/.eslintrc index eee34ab2..0403cb1a 100644 --- a/test/.eslintrc +++ b/test/.eslintrc @@ -3,10 +3,11 @@ "jest": true }, "rules": { + "fp/no-loops": "off", "global-require": "off", "import/no-dynamic-require": "off", - "fp/no-loops": "off", "no-restricted-syntax": "off", - "no-await-in-loop": "off" + "no-await-in-loop": "off", + "react/jsx-filename-extension": "off" } } diff --git a/yarn.lock b/yarn.lock index c32646a4..edda07ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3105,13 +3105,7 @@ dot-prop@^4.1.0: dependencies: is-obj "^1.0.0" -dotenv-webpack@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/dotenv-webpack/-/dotenv-webpack-1.5.0.tgz#934b218d1d87158237aa9cba0b3becf63298cb4b" - dependencies: - dotenv "^4.0.0" - -dotenv@4.0.0, dotenv@^4.0.0: +dotenv@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d" @@ -3161,22 +3155,22 @@ ejs@^2.5.6, ejs@~2.5.6: version "2.5.6" resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.6.tgz#479636bfa3fe3b1debd52087f0acb204b4f19c88" -electron-builder-http@19.7.0, electron-builder-http@~19.7.0: - version "19.7.0" - resolved "https://registry.yarnpkg.com/electron-builder-http/-/electron-builder-http-19.7.0.tgz#619e1015200127952e846cbc18f2c54b0edb3f2c" +electron-builder-http@19.7.2, electron-builder-http@~19.7.2: + version "19.7.2" + resolved "https://registry.yarnpkg.com/electron-builder-http/-/electron-builder-http-19.7.2.tgz#5a63566e4a4685413861eaf087dcaa9a0e362a03" dependencies: - debug "2.6.8" + debug "^2.6.8" fs-extra-p "^4.3.0" -electron-builder-util@19.7.0, electron-builder-util@~19.7.0: - version "19.7.0" - resolved "https://registry.yarnpkg.com/electron-builder-util/-/electron-builder-util-19.7.0.tgz#c7ce4297e5ea8a50b08ad571fc88563a4e455adc" +electron-builder-util@19.8.0, electron-builder-util@~19.8.0: + version "19.8.0" + resolved "https://registry.yarnpkg.com/electron-builder-util/-/electron-builder-util-19.8.0.tgz#f6aa75c271cafcc335b965b50416d167e74ad6c5" dependencies: "7zip-bin" "^2.1.0" bluebird-lst "^1.0.2" chalk "^1.1.3" - debug "2.6.8" - electron-builder-http "~19.7.0" + debug "^2.6.8" + electron-builder-http "~19.7.2" fcopy-pre-bundled "0.3.4" fs-extra-p "^4.3.0" ini "^1.3.4" @@ -3186,9 +3180,9 @@ electron-builder-util@19.7.0, electron-builder-util@~19.7.0: stat-mode "^0.2.2" tunnel-agent "^0.6.0" -electron-builder@19.7.1: - version "19.7.1" - resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-19.7.1.tgz#5f81c097398aaf0b3224f1151341592952cdd986" +electron-builder@^19.8.0: + version "19.8.0" + resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-19.8.0.tgz#32f0a008be809e1df021ca13a8d9e92f6ee5c917" dependencies: "7zip-bin" "^2.1.0" ajv "^5.2.0" @@ -3198,12 +3192,12 @@ electron-builder@19.7.1: chalk "^1.1.3" chromium-pickle-js "^0.2.0" cuint "^0.2.2" - debug "2.6.8" - electron-builder-http "19.7.0" - electron-builder-util "19.7.0" + debug "^2.6.8" + electron-builder-http "19.7.2" + electron-builder-util "19.8.0" electron-download-tf "4.3.1" electron-osx-sign "0.4.6" - electron-publish "19.7.0" + electron-publish "19.8.0" fs-extra-p "^4.3.0" hosted-git-info "^2.4.2" is-ci "^1.0.10" @@ -3227,12 +3221,12 @@ electron-chromedriver@~1.6.0: electron-download "^3.1.0" extract-zip "^1.6.0" -electron-debug@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/electron-debug/-/electron-debug-1.1.0.tgz#050a9c3f906fffc2492510cf8ac31d0f32a579e1" +electron-debug@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/electron-debug/-/electron-debug-1.2.0.tgz#22e51a73e1bf095d0bb51a6c3d97a203364c4222" dependencies: electron-is-dev "^0.1.0" - electron-localshortcut "^0.6.0" + electron-localshortcut "^2.0.0" electron-devtools-installer@2.2.0: version "2.2.0" @@ -3271,13 +3265,20 @@ electron-download@^3.0.1, electron-download@^3.1.0: semver "^5.3.0" sumchecker "^1.2.0" +electron-is-accelerator@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/electron-is-accelerator/-/electron-is-accelerator-0.1.2.tgz#509e510c26a56b55e17f863a4b04e111846ab27b" + electron-is-dev@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-0.1.2.tgz#8a1043e32b3a1da1c3f553dce28ce764246167e3" -electron-localshortcut@^0.6.0: - version "0.6.1" - resolved "https://registry.yarnpkg.com/electron-localshortcut/-/electron-localshortcut-0.6.1.tgz#c4e268c38a6e42f40de5618fc906d1ed608f11aa" +electron-localshortcut@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/electron-localshortcut/-/electron-localshortcut-2.0.2.tgz#6a1adcd6514c957328ec7912f5ccb5e1c10706db" + dependencies: + debug "^2.6.8" + electron-is-accelerator "^0.1.0" electron-osx-sign@0.4.6: version "0.4.6" @@ -3291,14 +3292,14 @@ electron-osx-sign@0.4.6: plist "^2.0.1" tempfile "^1.1.1" -electron-publish@19.7.0: - version "19.7.0" - resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-19.7.0.tgz#28056db1a749646ce9ad50b45947abd8d3fda7c3" +electron-publish@19.8.0: + version "19.8.0" + resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-19.8.0.tgz#ff288f53f639553791a727dfcad1b9d15c999365" dependencies: bluebird-lst "^1.0.2" chalk "^1.1.3" - electron-builder-http "~19.7.0" - electron-builder-util "~19.7.0" + electron-builder-http "~19.7.2" + electron-builder-util "~19.8.0" fs-extra-p "^4.3.0" mime "^1.3.6" @@ -3564,6 +3565,12 @@ eslint-config-bliss@^1.0.9: flow-bin "^0.48.0" prettier "^1.4.4" +eslint-config-prettier@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-2.2.0.tgz#ca47663852789a75c10feba673e802cc1eff085f" + dependencies: + get-stdin "^5.0.1" + eslint-formatter-pretty@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/eslint-formatter-pretty/-/eslint-formatter-pretty-1.1.0.tgz#ab4d06da02fed8c13ae9f0dc540a433ef7ed6f5e" @@ -3591,12 +3598,12 @@ eslint-import-resolver-node@^0.2.0: object-assign "^4.0.1" resolve "^1.1.6" -eslint-import-resolver-webpack@0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.8.1.tgz#c7f8b4d5bd3c5b489457e5728c5db1c4ffbac9aa" +eslint-import-resolver-webpack@^0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.8.3.tgz#ad61e28df378a474459d953f246fd43f92675385" dependencies: array-find "^1.0.0" - debug "^2.2.0" + debug "^2.6.8" enhanced-resolve "~0.9.0" find-root "^0.1.1" has "^1.0.1" @@ -3667,13 +3674,13 @@ eslint-plugin-fp@^2.3.0: lodash "^4.13.1" req-all "^0.1.0" -eslint-plugin-import@2.3.0, eslint-plugin-import@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.3.0.tgz#37c801e0ada0e296cbdf20c3f393acb5b52af36b" +eslint-plugin-import@^2.3.0, eslint-plugin-import@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.6.0.tgz#2a4bbad36a078e052a3c830ce3dfbd6b8a12c6e5" dependencies: builtin-modules "^1.1.1" contains-path "^0.1.0" - debug "^2.2.0" + debug "^2.6.8" doctrine "1.5.0" eslint-import-resolver-node "^0.2.0" eslint-module-utils "^2.0.0"