...
 
Commits (4)
......@@ -38,7 +38,6 @@ class App extends Component {
<Route path='/system/login' component={DismissMessages(import('./pages/Login'))}/>
<Route path='/system/config' component={AuthRequired(import('./pages/Config'))}/>
<Route path='/system/karas/download' component={AuthRequired(import('./pages/Karas/KaraDownload'))}/>
<Route path="/system/karas/manage" component={AuthRequired(import('./pages/Karas/ManageKaras'))}/>
<Route path='/system/karas/create' component={AuthRequired(import('./pages/Karas/KaraEdit'))}/>
<Route path='/system/karas/history' component={AuthRequired(import('./pages/Karas/History'))}/>
<Route path='/system/karas/ranking' component={AuthRequired(import('./pages/Karas/Ranking'))}/>
......
import got from 'got';
// GET recent karas from kara.moe
export async function getRecentKaras() {
try {
const res = await got(
'https://kara.moe/api/karas/recent',
{ json : true }
);
return res.body.content ? res.body.content : [];
} catch (e) {
console.log(
`Error from downloadManager.js:getRecentKaras() - ${e.response.status}`
);
throw e;
return [];
}
}
export async function getKarasBySearchString(searchString) {
try {
const res = await got(
`https://kara.moe/api/karas?filter=${searchString}`,
{ json : true }
);
return res.body.content ? res.body.content : [];
} catch (e) {
console.log(
`Error from downloadManager.js:getKarasBySearchString() - ${e.response.status}`
);
throw e;
return [];
}
}
\ No newline at end of file
import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import { routerReducer, routerMiddleware } from 'react-router-redux';
import createHistory from 'history/createBrowserHistory';
import createSagaMiddleware from 'redux-saga';
import rootSaga from './sagas/sagas';
import navigation from './reducers/navigation';
import auth from './reducers/auth';
import karas from './reducers/karas';
//import karas from './reducers/karas';
const sagaMiddleware = createSagaMiddleware();
const initialState = {};
export const history = createHistory();
const middleware = [routerMiddleware(history), sagaMiddleware];
const middleware = [routerMiddleware(history)];
// If devtools is present use it's compose instead of redux's compose; Does the same thing
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
......@@ -20,7 +19,7 @@ export default () => {
const reducers = combineReducers({
navigation,
auth,
karas,
//karas,
router: routerReducer
});
const store = createStore(
......@@ -28,6 +27,5 @@ export default () => {
initialState, // default state
composeEnhancers(applyMiddleware(...middleware))
);
sagaMiddleware.run(rootSaga);
return store;
};
......@@ -69,7 +69,6 @@ class KMMenu extends Component {
<Menu.Item key='karalist'><Link to='/system/karas'>List</Link></Menu.Item>
<Menu.Item key='karaimport'><Link to='/system/karas/create'>New</Link></Menu.Item>
<Menu.Item key='karadownload'><Link to='/system/karas/download'>Download</Link></Menu.Item>
<Menu.Item key='karamanage'><Link to='/system/karas/manage'>Manage</Link></Menu.Item>
<Menu.Item key='karahistory'><Link to='/system/karas/history'>History</Link></Menu.Item>
<Menu.Item key='kararanking'><Link to='/system/karas/ranking'>Most requested</Link></Menu.Item>
<Menu.Item key='karaviewcounts'><Link to='/system/karas/viewcounts'>Most played</Link></Menu.Item>
......
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Tabs, List, Button, Badge } from 'antd';
import { filterLocalKaras, deleteKara as dk } from '../../actions/karas';
import DownloadKaras from './ManageKaras/DownloadKaras';
import DownloadQueueKaras from './ManageKaras/DownloadQueueKaras';
const TabPane = Tabs.TabPane;
const ListItem = List.Item;
class ManageKaras extends Component {
componentDidMount() {}
componentDidUpdate() {
// Set a timer to check updates again?
}
deleteSong = kid => {
const { deleteKara } = this.props;
deleteKara(kid);
};
render() {
const { localKaras, downloadQueueCount } = this.props;
const renderLocalKaras = ({ kid, title }) => (
<ListItem
key={kid}
actions={[
<Button type="danger" onClick={() => this.deleteSong(kid)}>
Delete
</Button>
]}
>
<ListItem.Meta title={title} />
</ListItem>
);
return (
<div style={{ height: '100%', overflow: 'auto' }}>
<Tabs defaultActiveKey="1" style={{ padding: 24 }}>
<TabPane tab="Local karas" key="1">
<List dataSource={localKaras} renderItem={renderLocalKaras} pagination={{position:'both',pageSize:50}}/>
</TabPane>
<TabPane tab="Get more karas" key="2">
<DownloadKaras />
</TabPane>
<TabPane
tab={
<span>
Download Queue{' '}
<Badge
count={downloadQueueCount}
style={{ backgroundColor: '#40a9ff' }}
/>
</span>
}
key="3"
>
<DownloadQueueKaras />
</TabPane>
</Tabs>
</div>
);
}
}
const mapStateToProps = state => {
const { karas } = state;
return {
localKaras: karas.localKaras,
onlineKaras: karas.onlineKaras,
isSearching: karas.isSearching,
downloadQueueCount: karas.downloadQueue.length
};
};
const mapDispatchToProps = {
filterLocalKaras,
deleteKara: dk
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(ManageKaras);
import React, { Component } from 'react';
import { List, Button } from 'antd';
import { connect } from 'react-redux';
import { filterOnlineKaras as fok, downloadSong as dls } from '../../../actions/karas';
const ListItem = List.Item;
class DownloadKaras extends Component {
state = {
filter: {
searchString: ''
}
};
componentDidMount() {
const { filterOnlineKaras } = this.props;
filterOnlineKaras();
}
searchChange_delay = null;
searchChange = e => {
const newState = {
...this.state,
filter: {
searchString: e.target.value
}
};
const { filter } = newState;
const { filterOnlineKaras } = this.props;
this.setState(newState, () => {
clearTimeout(this.searchChange_delay);
this.searchChange_delay = setTimeout((e) => {
filterOnlineKaras(filter);
},500, filter)
});
};
downloadSong = kid => {
this.props.downloadSong(kid);
};
renderOnlineKaras = ({ kid, title }) => (
<ListItem
key={kid}
actions={[
<Button onClick={() => this.downloadSong(kid)}>Download</Button>
]}
>
<ListItem.Meta title={title} />
</ListItem>
);
render() {
const { onlineKaras, isSearching } = this.props;
const {
filter: { searchString }
} = this.state;
return (
<div>
<input type="text" onChange={this.searchChange} value={searchString} />
<List
dataSource={onlineKaras}
renderItem={this.renderOnlineKaras}
loading={isSearching}
pagination={{position:'both',pageSize:50}}
/>
</div>
);
}
}
const mapStateToProps = state => {
const { karas } = state;
const { onlineKaras, isSearching } = karas;
return {
onlineKaras,
isSearching
};
};
const mapDispatchToProps = {
filterOnlineKaras: fok,
downloadSong: dls
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(DownloadKaras);
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { List, Button, Progress } from 'antd';
import { downloadStart as dstart, downloadPause as dpause } from '../../../actions/karas';
const ListItem = List.Item;
class DownloadQueueKaras extends Component {
constructor(props) {
super(props);
}
componentDidMount() {}
downloadStart = () => {
this.props.downloadStart();
};
downloadPause = () => {
this.props.downloadPause();
};
renderDownloadQueue(item) {
const { status } = item;
const inProgress = item.hasOwnProperty('progress') && status === 'DL_RUNNING';
let progress = 0;
if (inProgress) {
const { current, total } = item.progress;
if (total > 10000) {
progress = Math.floor((current / total) * 100);
} else {
progress = 100;
}
} else if (status === 'DL_DONE') {
progress = 100;
}
return (
<ListItem key={item.pk_id_download}>
<ListItem.Meta title={item.title} />
<ListItem.Meta title={item.status} />
<Progress percent={progress} />
</ListItem>
);
}
render() {
console.log(this.props.downloadSong)
const { downloadQueue } = this.props;
return (
<div>
<Button onClick={() => this.downloadStart()}>Start</Button>
<List
dataSource={downloadQueue}
renderItem={this.renderDownloadQueue}
loading={this.props.isSearching}
/>
</div>
);
}
}
const mapStateToProps = state => {
const { karas } = state;
const { downloadQueue } = karas;
return {
downloadQueue
};
};
const mapDispatchToProps = {
downloadStart: dstart,
downloadPause: dpause
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(DownloadQueueKaras);
import { delay, eventChannel } from 'redux-saga';
import pick from 'lodash/pick';
import equal from 'fast-deep-equal';
import {
put,
takeLatest,
takeEvery,
select,
call,
fork,
take
} from 'redux-saga/effects';
import openSocket from 'socket.io-client';
import {
KARAS_FILTER_ONLINE,
loadOnlineKaras,
setIsSearching,
KARAS_DOWNLOAD_ADD_TO_QUEUE,
KARAS_LOAD_DOWNLOAD_QUEUE,
KARAS_DOWNLOAD_PROGRESS_UPDATE,
KARAS_DOWNLOAD_START,
KARAS_DOWNLOAD_PAUSE
} from '../actions/karas';
import { getDownloadQueue, postToDownloadQueue, putToDownloadQueueStart, putToDownloadQueuePause } from '../api/local';
import { getRecentKaras, getKarasBySearchString } from '../api/online';
import { fetchLocalKara, fetchDownloadQueue } from '../reducers/karas';
function downloadQueueChannel() {
return eventChannel(emit => {
setTimeout(async () => {
const res = await getDownloadQueue();
emit(res);
}, 1000)
const iv = setInterval(async () => {
const res = await getDownloadQueue();
emit(res);
}, 1000);
return () => clearInterval(iv);
});
}
function downloadProgressChannel() {
return eventChannel(emit => {
const downloadSocket = openSocket('http://localhost:1337');
downloadSocket.on('downloadProgress', data => {
emit(data);
});
downloadSocket.on('downloadBatchProgress', data => {});
return () => {
downloadSocket.close();
};
});
}
/***************************** Subroutines ************************************/
// Runs a get request to kara.moe's recent songs then loads it into the store
function* filterOnlineKaras(action) {
yield put(setIsSearching(true));
yield delay(500);
const filter = action.payload;
let onlineKaras;
if (filter) {
const { searchString } = filter;
if (searchString) {
onlineKaras = yield getKarasBySearchString(searchString);
} else {
onlineKaras = yield getRecentKaras(); // TODO Not yet the cleanest implementation
}
} else {
onlineKaras = [];
}
console.log('filterOnlineKaras',onlineKaras);
onlineKaras.forEach(k => {
k.name = k.subfile.replace('.ass', '');
});
yield put(setIsSearching(false));
yield put(loadOnlineKaras(onlineKaras));
}
function* watchDownloadQueue() {
// TODO: Should use/separate a redux selector
const channel = yield call(downloadQueueChannel);
while (true) {
const latestQueue = yield take(channel);
for (let dlItem of latestQueue) {
const kara = yield select(fetchLocalKara, dlItem.name);
if (kara) {
dlItem.title = kara.title;
} else {
dlItem.title = dlItem.name;
}
}
const currentQueue = yield select(state => state.karas.downloadQueue);
const latestPkIds = latestQueue.map(i => i.pk_id_download);
const currentPkIds = currentQueue.map(i => i.pk_id_download);
const areAllDone = !latestQueue.some(i => i.status !== 'DL_DONE');
const sameItems = equal(latestPkIds, currentPkIds);
if (!sameItems || (sameItems && areAllDone)) {
yield put({
type: KARAS_LOAD_DOWNLOAD_QUEUE,
payload: latestQueue
});
}
}
}
function* watchDownloadProgressUpdates() {
const channel = yield call(downloadProgressChannel);
while (true) {
yield delay(500);
const downloadProgress = yield take(channel);
const { value, total: t } = downloadProgress;
const total = parseInt(t);
// For now using this to determine whether to show progress in redux
// if (total > 30000) {
const downloadQueue = yield select(fetchDownloadQueue);
const updatedDownloadQueue = [...downloadQueue];
const indexOfUpdate = updatedDownloadQueue.findIndex(
dlItem => dlItem.name === downloadProgress.id
);
updatedDownloadQueue[indexOfUpdate] = {
...updatedDownloadQueue[indexOfUpdate],
progress: {
total,
current: value
}
};
yield put({
type: KARAS_DOWNLOAD_PROGRESS_UPDATE,
payload: updatedDownloadQueue
});
// }
}
}
function* addToDownloadQueue(action) {
const kid = action.payload;
const onlineKaras = yield select(state => state.karas.onlineKaras);
const kara = onlineKaras.find(k => k.kid === kid);
const downloadObject = pick(kara, [
'mediafile',
'subfile',
'karafile',
'seriefiles'
]);
downloadObject.size = kara.mediasize;
downloadObject.name = kara.name;
yield call(postToDownloadQueue, 'kara.moe', [downloadObject]);
// const latestQueue = yield call(getDownloadQueue);
// yield put({
// type: KARAS_LOAD_DOWNLOAD_QUEUE,
// payload: latestQueue
// });
}
function* startDownloadQueue(action) {
yield call(putToDownloadQueueStart);
}
function* pauseDownloadQueue(action) {
yield call(putToDownloadQueuePause);
}
export default function* downloadManager() {
yield fork(watchDownloadQueue);
yield fork(watchDownloadProgressUpdates);
yield takeLatest(KARAS_FILTER_ONLINE, filterOnlineKaras);
yield takeEvery(KARAS_DOWNLOAD_ADD_TO_QUEUE, addToDownloadQueue);
yield takeEvery(KARAS_DOWNLOAD_START, startDownloadQueue);
yield takeEvery(KARAS_DOWNLOAD_PAUSE, pauseDownloadQueue);
}
import { delay, eventChannel, END } from 'redux-saga';
import {
put,
call,
takeLatest,
takeEvery,
fork,
select,
take
} from 'redux-saga/effects';
import equal from 'fast-deep-equal';
import {
KARAS_FILTER_LOCAL,
KARAS_DELETE_KARA,
loadLocalKaras
} from '../actions/karas';
import { fetchLocalKaras, fetchLocalKara } from '../reducers/karas';
import { getLocalKaras, deleteKaraByLocalId } from '../api/local';
function localKarasChannel() {
return eventChannel(emit => {
setTimeout(async () => {
const res = await getLocalKaras();
emit(res);
}, 1000);
const iv = setInterval(async () => {
const res = await getLocalKaras();
emit(res);
}, 10000);
return () => clearInterval(iv);
});
}
function* watchLocalKaras() {
const channel = yield call(localKarasChannel);
while (true) {
const response = yield take(channel);
const localKaras = yield select(fetchLocalKaras);
if (!equal(response, localKaras)) {
yield put(loadLocalKaras(response));
}
}
}
function* filterLocalKaras(action) {
// TODO: Pass in the filter object
const localKaras = yield getLocalKaras();
try {
yield put(loadLocalKaras(localKaras));
} catch (e) {
console.log(e);
}
}
function* deleteKara(action) {
const kid = action.payload;
const kara = yield select(fetchLocalKara, kid);
try {
yield call(deleteKaraByLocalId, kara.kara_id);
} catch (e) {
console.log(e);
}
}
export default function*() {
// Start the watcher
yield fork(watchLocalKaras);
yield takeLatest(KARAS_FILTER_LOCAL, filterLocalKaras);
yield takeEvery(KARAS_DELETE_KARA, deleteKara);
}
import { all, call } from 'redux-saga/effects';
import localKarasSaga from './localKaras';
import downloadManagerSaga from './downloadManager';
export default function* root() {
//yield all([call(localKarasSaga), call(downloadManagerSaga)]);
}
......@@ -98,21 +98,31 @@ async function processDownload(download) {
url: download.urls.kara.remote,
id: download.name
});
for (const serie of download.urls.serie) {
list.push({
filename: resolve(tempSeriesPath, serie.local),
url: serie.remote,
id: download.name
});
bundle.series.push(resolve(localSeriesPath, serie.local));
for (const serie of download.urls.serie)
{
if(typeof serie.local == 'string')
{
list.push({
filename: resolve(tempSeriesPath, serie.local),
url: serie.remote,
id: download.name
});
bundle.series.push(resolve(localSeriesPath, serie.local));
}
}
await downloadFiles(download, list);
// Delete files if they're already present
await asyncMove(tempMedia, localMedia, {overwrite: true});
if (download.urls.lyrics.local !== 'dummy.ass') await asyncMove(tempLyrics, localLyrics, {overwrite: true});
await asyncMove(tempKara, localKara, {overwrite: true});
for (const seriefile of download.urls.serie) {
await asyncMove(resolve(tempSeriesPath, seriefile.local), resolve(localSeriesPath, seriefile.local), {overwrite: true});
for (const seriefile of download.urls.serie)
{
if(typeof seriefile.local == 'string')
{
await asyncMove(resolve(tempSeriesPath, seriefile.local), resolve(localSeriesPath, seriefile.local), {overwrite: true});
}
}
logger.info(`[Download] Finished downloading item "${download.name}"`);
// Now adding our newly downloaded kara
......
......@@ -60,11 +60,13 @@ export async function addSerie(serieObj) {
if (serie) throw 'Series original name already exists';
if (!serieObj.sid) serieObj.sid = uuidV4();
if (!serieObj.seriefile) serieObj.seriefile = `${sanitizeFile(serieObj.name)}.series.json`;
await insertSerie(serieObj),
await Promise.all([
insertSerie(serieObj),
insertSeriei18n(serieObj),
writeSeriesFile(serieObj)
]);
compareKarasChecksum(true);
await refreshSeries();
refreshKaraSeries().then(refreshKaras());
......