Merge pull request #408 from thornbill/transcode-cleanup

Report to server when download stops
This commit is contained in:
Bill Thornton 2022-12-02 11:46:12 -05:00 committed by GitHub
commit 94e33b69bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 106 additions and 5 deletions

21
App.js
View File

@ -8,6 +8,7 @@
import 'react-native-url-polyfill/auto';
import { Ionicons } from '@expo/vector-icons';
import { getPlaystateApi } from '@jellyfin/sdk/lib/utils/api/playstate-api';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { NavigationContainer } from '@react-navigation/native';
import { Asset } from 'expo-asset';
@ -20,7 +21,7 @@ import { observer } from 'mobx-react-lite';
import { AsyncTrunk } from 'mobx-sync-lite';
import PropTypes from 'prop-types';
import React, { useContext, useEffect, useState } from 'react';
import { useColorScheme } from 'react-native';
import { Alert, useColorScheme } from 'react-native';
import { ThemeContext, ThemeProvider } from 'react-native-elements';
import { SafeAreaProvider } from 'react-native-safe-area-context';
@ -133,6 +134,7 @@ const App = observer(({ skipLoadingScreen }) => {
// TODO: The resumable should be saved to allow pausing/resuming downloads
// Download the file
try {
download.isDownloading = true;
await resumable.downloadAsync();
@ -140,8 +142,25 @@ const App = observer(({ skipLoadingScreen }) => {
download.isDownloading = false;
} catch (e) {
console.error('[App] Download failed', e);
Alert.alert('Download Failed', `"${download.title}" failed to download.`);
// TODO: If a download fails, we should probably remove it from the queue
download.isDownloading = false;
}
// Report download has stopped
const serverUrl = download.serverUrl.endsWith('/') ? download.serverUrl.slice(0, -1) : download.serverUrl;
const api = rootStore.sdk.createApi(serverUrl, download.apiKey);
console.log('[App] Reporting download stopped', download.sessionId);
getPlaystateApi(api)
.reportPlaybackStopped({
playbackStopInfo: {
PlaySessionId: download.sessionId
}
})
.catch(err => {
console.error('[App] Failed reporting download stopped', err.response || err.request || err.message);
});
};
rootStore.downloadStore.downloads

View File

@ -47,3 +47,11 @@ jest.mock('@react-navigation/native/lib/commonjs/useLinking.native', () => ({
/* Safe Area Context Mocks */
import mockSafeAreaContext from 'react-native-safe-area-context/jest/mock';
jest.mock('react-native-safe-area-context', () => mockSafeAreaContext);
/* UUID Mocks */
jest.mock('uuid', () => {
let value = 0;
return {
v4: () => `uuid-${value++}`
};
});

View File

@ -7,6 +7,7 @@
import * as FileSystem from 'expo-file-system';
import { computed, decorate, observable } from 'mobx';
import { ignore } from 'mobx-sync-lite';
import { v4 as uuidv4 } from 'uuid';
export default class DownloadModel {
isComplete = false
@ -15,6 +16,8 @@ export default class DownloadModel {
apiKey: string
itemId: string
/** The "play" session ID for reporting a download has stopped. */
sessionId = uuidv4()
serverId: string
serverUrl: string
@ -61,6 +64,7 @@ export default class DownloadModel {
const streamParams = new URLSearchParams({
deviceId,
api_key: this.apiKey,
playSessionId: this.sessionId,
// TODO: add mediaSourceId to support alternate media versions
videoCodec: 'hevc,h264',
audioCodec: 'aac,mp3,ac3,eac3,flac,alac',
@ -79,6 +83,7 @@ decorate(DownloadModel, {
isNew: observable,
apiKey: observable,
itemId: observable,
sessionId: observable,
serverId: observable,
serverUrl: observable,
title: observable,

View File

@ -26,6 +26,7 @@ describe('DownloadModel', () => {
expect(download.apiKey).toBe('api-key');
expect(download.itemId).toBe('item-id');
expect(download.sessionId).toBe('uuid-0');
expect(download.serverId).toBe('server-id');
expect(download.serverUrl).toBe('https://example.com/');
@ -42,6 +43,6 @@ describe('DownloadModel', () => {
expect(download.localFilename).toBe('file name.mp4');
expect(download.localPath).toBe(`${DOCUMENT_DIRECTORY}server-id/item-id/`);
expect(download.uri).toBe(`${DOCUMENT_DIRECTORY}server-id/item-id/file%20name.mp4`);
expect(download.getStreamUrl('device-id').toString()).toBe('https://example.com/Videos/item-id/stream.mp4?deviceId=device-id&api_key=api-key&videoCodec=hevc%2Ch264&audioCodec=aac%2Cmp3%2Cac3%2Ceac3%2Cflac%2Calac&maxAudioChannels=6');
expect(download.getStreamUrl('device-id').toString()).toBe('https://example.com/Videos/item-id/stream.mp4?deviceId=device-id&api_key=api-key&playSessionId=uuid-0&videoCodec=hevc%2Ch264&audioCodec=aac%2Cmp3%2Cac3%2Ceac3%2Cflac%2Calac&maxAudioChannels=6');
});
});

44
package-lock.json generated
View File

@ -2126,6 +2126,41 @@
"integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
"dev": true
},
"@jellyfin/sdk": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@jellyfin/sdk/-/sdk-0.7.0.tgz",
"integrity": "sha512-GNoGv+2qY+xK7WpO7sUUNpZvzgN7RwXMyOhIy9mE/LdDSr6bqZHwrzT1Pv0+vUW7Epw67bwIMWuYivyBYejEHw==",
"requires": {
"axios": "0.27.2",
"compare-versions": "5.0.1"
},
"dependencies": {
"axios": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"requires": {
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
}
},
"compare-versions": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-5.0.1.tgz",
"integrity": "sha512-v8Au3l0b+Nwkp4G142JcgJFh1/TUhdxut7wzD1Nq1dyp5oa3tXaqb03EXOAB6jS4gMlalkjAUPZBMiAfKUixHQ=="
},
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
}
}
},
"@jest/console": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/@jest/console/-/console-26.6.2.tgz",
@ -3694,6 +3729,12 @@
}
}
},
"@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
"dev": true
},
"@types/webpack": {
"version": "4.41.33",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.33.tgz",
@ -9320,8 +9361,7 @@
"follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"dev": true
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
},
"fontfaceobserver": {
"version": "2.3.0",

View File

@ -34,6 +34,7 @@
"Android >= 5"
],
"dependencies": {
"@jellyfin/sdk": "^0.7.0",
"@react-native-async-storage/async-storage": "~1.17.3",
"@react-native-community/masked-view": "0.1.10",
"@react-navigation/bottom-tabs": "^6.0.7",
@ -82,6 +83,7 @@
"@testing-library/react-native": "^7.2.0",
"@types/jest": "^27.0.2",
"@types/react": "~17.0.21",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.16.0",
"@typescript-eslint/parser": "^4.33.0",
"babel-preset-expo": "~9.1.0",

View File

@ -33,6 +33,7 @@ exports[`DownloadScreen should render correctly 1`] = `
"itemId": "item-id",
"serverId": "server-id",
"serverUrl": "https://example.com/",
"sessionId": "uuid-0",
"title": "title",
},
Object {
@ -45,6 +46,7 @@ exports[`DownloadScreen should render correctly 1`] = `
"itemId": "item-id-2",
"serverId": "server-id",
"serverUrl": "https://test2.example.com/",
"sessionId": "uuid-1",
"title": "other title",
},
]

View File

@ -7,10 +7,14 @@
// polyfill crypto.getRandomValues
import 'react-native-get-random-values';
import { action, decorate, observable } from 'mobx';
import { Jellyfin } from '@jellyfin/sdk';
import Constants from 'expo-constants';
import { action, computed, decorate, observable } from 'mobx';
import { ignore } from 'mobx-sync-lite';
import { v4 as uuidv4 } from 'uuid';
import { getAppName, getSafeDeviceName } from '../utils/Device';
import DownloadStore from './DownloadStore';
import MediaStore from './MediaStore';
import ServerStore from './ServerStore';
@ -47,6 +51,19 @@ export default class RootStore {
serverStore = new ServerStore()
settingStore = new SettingStore()
get sdk() {
return new Jellyfin({
clientInfo: {
name: getAppName(),
version: Constants.nativeAppVersion
},
deviceInfo: {
name: getSafeDeviceName(),
id: this.deviceId
}
});
}
reset() {
this.deviceId = uuidv4();
@ -69,5 +86,6 @@ decorate(RootStore, {
isFullscreen: [ ignore, observable ],
isReloadRequired: [ ignore, observable ],
didPlayerCloseManually: [ ignore, observable ],
sdk: computed,
reset: action
});

View File

@ -3,6 +3,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Jellyfin } from '@jellyfin/sdk';
import DownloadStore from '../DownloadStore';
import MediaStore from '../MediaStore';
import RootStore from '../RootStore';
@ -17,6 +19,10 @@ describe('RootStore', () => {
expect(store.isFullscreen).toBe(false);
expect(store.isReloadRequired).toBe(false);
expect(store.didPlayerCloseManually).toBe(true);
expect(store.sdk).toBeInstanceOf(Jellyfin);
expect(store.sdk.deviceInfo.id).toBe(store.deviceId);
expect(store.downloadStore).toBeInstanceOf(DownloadStore);
expect(store.mediaStore).toBeInstanceOf(MediaStore);
expect(store.serverStore).toBeInstanceOf(ServerStore);