diff --git a/CHANGELOG.md b/CHANGELOG.md
index d845d2f3e..fa5a2032d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,11 @@
* Added `video.uuid` field
* Added `video.url` field
+### Features
+
+ * Add "Local" in menu that lists only local videos
+
+
## v1.0.0-alpha.4
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html
index d174c76ba..d827a4dd4 100644
--- a/client/src/app/menu/menu.component.html
+++ b/client/src/app/menu/menu.component.html
@@ -43,6 +43,11 @@
Recently added
+
+
+
+ Local
+
diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss
index 1f3c889cf..da5a581a1 100644
--- a/client/src/app/menu/menu.component.scss
+++ b/client/src/app/menu/menu.component.scss
@@ -126,6 +126,14 @@ menu {
background-image: url('../../assets/images/menu/recently-added.svg');
}
+ &.icon-videos-local {
+ width: 23px;
+ height: 23px;
+ position: relative;
+ top: -1px;
+ background-image: url('../../assets/images/menu/home.svg');
+ }
+
&.icon-administration {
width: 23px;
height: 23px;
diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts
index a151a2983..0a8894fd9 100644
--- a/client/src/app/shared/video/video.service.ts
+++ b/client/src/app/shared/video/video.service.ts
@@ -7,6 +7,7 @@ import { Video as VideoServerModel, VideoDetails as VideoDetailsServerModel } fr
import { ResultList } from '../../../../../shared/models/result-list.model'
import { UserVideoRateUpdate } from '../../../../../shared/models/videos/user-video-rate-update.model'
import { UserVideoRate } from '../../../../../shared/models/videos/user-video-rate.model'
+import { VideoFilter } from '../../../../../shared/models/videos/video-query.type'
import { VideoRateType } from '../../../../../shared/models/videos/video-rate.type'
import { VideoUpdate } from '../../../../../shared/models/videos/video-update.model'
import { environment } from '../../../environments/environment'
@@ -94,12 +95,20 @@ export class VideoService {
.catch((res) => this.restExtractor.handleError(res))
}
- getVideos (videoPagination: ComponentPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> {
+ getVideos (
+ videoPagination: ComponentPagination,
+ sort: SortField,
+ filter?: VideoFilter
+ ): Observable<{ videos: Video[], totalVideos: number}> {
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
let params = new HttpParams()
params = this.restService.addRestGetParams(params, pagination, sort)
+ if (filter) {
+ params = params.set('filter', filter)
+ }
+
return this.authHttp
.get(VideoService.BASE_VIDEO_URL, { params })
.map(this.extractVideos)
diff --git a/client/src/app/videos/video-list/video-local.component.ts b/client/src/app/videos/video-list/video-local.component.ts
new file mode 100644
index 000000000..8cac2c12c
--- /dev/null
+++ b/client/src/app/videos/video-list/video-local.component.ts
@@ -0,0 +1,37 @@
+import { Component, OnInit } from '@angular/core'
+import { ActivatedRoute, Router } from '@angular/router'
+import { immutableAssign } from '@app/shared/misc/utils'
+import { NotificationsService } from 'angular2-notifications'
+import { AuthService } from '../../core/auth'
+import { AbstractVideoList } from '../../shared/video/abstract-video-list'
+import { SortField } from '../../shared/video/sort-field.type'
+import { VideoService } from '../../shared/video/video.service'
+
+@Component({
+ selector: 'my-videos-local',
+ styleUrls: [ '../../shared/video/abstract-video-list.scss' ],
+ templateUrl: '../../shared/video/abstract-video-list.html'
+})
+export class VideoLocalComponent extends AbstractVideoList implements OnInit {
+ titlePage = 'Local videos'
+ currentRoute = '/videos/local'
+ sort = '-createdAt' as SortField
+
+ constructor (protected router: Router,
+ protected route: ActivatedRoute,
+ protected notificationsService: NotificationsService,
+ protected authService: AuthService,
+ private videoService: VideoService) {
+ super()
+ }
+
+ ngOnInit () {
+ super.ngOnInit()
+ }
+
+ getVideosObservable (page: number) {
+ const newPagination = immutableAssign(this.pagination, { currentPage: page })
+
+ return this.videoService.getVideos(newPagination, this.sort, 'local')
+ }
+}
diff --git a/client/src/app/videos/videos-routing.module.ts b/client/src/app/videos/videos-routing.module.ts
index 29ec5fd4f..561137b70 100644
--- a/client/src/app/videos/videos-routing.module.ts
+++ b/client/src/app/videos/videos-routing.module.ts
@@ -1,5 +1,6 @@
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
+import { VideoLocalComponent } from '@app/videos/video-list/video-local.component'
import { MetaGuard } from '@ngx-meta/core'
import { VideoSearchComponent } from './video-list'
import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
@@ -35,6 +36,15 @@ const videosRoutes: Routes = [
}
}
},
+ {
+ path: 'local',
+ component: VideoLocalComponent,
+ data: {
+ meta: {
+ title: 'Local videos'
+ }
+ }
+ },
{
path: 'search',
component: VideoSearchComponent,
diff --git a/client/src/app/videos/videos.module.ts b/client/src/app/videos/videos.module.ts
index 4b14d1da8..7c3d457b3 100644
--- a/client/src/app/videos/videos.module.ts
+++ b/client/src/app/videos/videos.module.ts
@@ -1,4 +1,5 @@
import { NgModule } from '@angular/core'
+import { VideoLocalComponent } from '@app/videos/video-list/video-local.component'
import { SharedModule } from '../shared'
import { VideoSearchComponent } from './video-list'
import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
@@ -17,6 +18,7 @@ import { VideosComponent } from './videos.component'
VideoTrendingComponent,
VideoRecentlyAddedComponent,
+ VideoLocalComponent,
VideoSearchComponent
],
diff --git a/client/src/assets/images/menu/home.svg b/client/src/assets/images/menu/home.svg
new file mode 100644
index 000000000..bb95e949a
--- /dev/null
+++ b/client/src/assets/images/menu/home.svg
@@ -0,0 +1,15 @@
+
+
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 10b309cd1..690872320 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -388,7 +388,7 @@ async function getVideoDescription (req: express.Request, res: express.Response)
}
async function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
- const resultList = await VideoModel.listForApi(req.query.start, req.query.count, req.query.sort)
+ const resultList = await VideoModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.filter)
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 0e5dd0d2f..14eb64102 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -29,6 +29,7 @@ import {
import { VideoPrivacy, VideoResolution } from '../../../shared'
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
import { Video, VideoDetails } from '../../../shared/models/videos'
+import { VideoFilter } from '../../../shared/models/videos/video-query.type'
import { activityPubCollection } from '../../helpers/activitypub'
import { createTorrentPromise, renamePromise, statPromise, unlinkPromise, writeFilePromise } from '../../helpers/core-utils'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
@@ -91,7 +92,7 @@ enum ScopeNames {
}
@Scopes({
- [ScopeNames.AVAILABLE_FOR_LIST]: (actorId: number) => ({
+ [ScopeNames.AVAILABLE_FOR_LIST]: (actorId: number, filter?: VideoFilter) => ({
where: {
id: {
[Sequelize.Op.notIn]: Sequelize.literal(
@@ -129,6 +130,7 @@ enum ScopeNames {
attributes: [ 'preferredUsername', 'url', 'serverId' ],
model: ActorModel.unscoped(),
required: true,
+ where: VideoModel.buildActorWhereWithFilter(filter),
include: [
{
attributes: [ 'host' ],
@@ -639,7 +641,7 @@ export class VideoModel extends Model
{
})
}
- static async listForApi (start: number, count: number, sort: string) {
+ static async listForApi (start: number, count: number, sort: string, filter?: VideoFilter) {
const query = {
offset: start,
limit: count,
@@ -648,7 +650,7 @@ export class VideoModel extends Model {
const serverActor = await getServerActor()
- return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id ] })
+ return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, filter ] })
.findAndCountAll(query)
.then(({ rows, count }) => {
return {
@@ -790,6 +792,16 @@ export class VideoModel extends Model {
}
}
+ private static buildActorWhereWithFilter (filter?: VideoFilter) {
+ if (filter && filter === 'local') {
+ return {
+ serverId: null
+ }
+ }
+
+ return {}
+ }
+
getOriginalFile () {
if (Array.isArray(this.VideoFiles) === false) return undefined
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts
index 3f6b82c50..42a1241f7 100644
--- a/server/tests/api/videos/multiple-servers.ts
+++ b/server/tests/api/videos/multiple-servers.ts
@@ -15,7 +15,7 @@ import {
dateIsValid,
doubleFollow,
flushAndRunMultipleServers,
- flushTests,
+ flushTests, getLocalVideos,
getVideo,
getVideoChannelsList,
getVideosList,
@@ -349,6 +349,36 @@ describe('Test multiple servers', function () {
})
})
+ describe('It should list local videos', function () {
+ it('Should list only local videos on server 1', async function () {
+ const { body } = await getLocalVideos(servers[0].url)
+
+ expect(body.total).to.equal(1)
+ expect(body.data).to.be.an('array')
+ expect(body.data.length).to.equal(1)
+ expect(body.data[0].name).to.equal('my super name for server 1')
+ })
+
+ it('Should list only local videos on server 2', async function () {
+ const { body } = await getLocalVideos(servers[1].url)
+
+ expect(body.total).to.equal(1)
+ expect(body.data).to.be.an('array')
+ expect(body.data.length).to.equal(1)
+ expect(body.data[0].name).to.equal('my super name for server 2')
+ })
+
+ it('Should list only local videos on server 3', async function () {
+ const { body } = await getLocalVideos(servers[2].url)
+
+ expect(body.total).to.equal(2)
+ expect(body.data).to.be.an('array')
+ expect(body.data.length).to.equal(2)
+ expect(body.data[0].name).to.equal('my super name for server 3')
+ expect(body.data[1].name).to.equal('my super name for server 3-2')
+ })
+ })
+
describe('Should seed the uploaded video', function () {
it('Should add the file 1 by asking server 3', async function () {
this.timeout(10000)
diff --git a/server/tests/utils/videos/videos.ts b/server/tests/utils/videos/videos.ts
index ec40c5465..89db16fec 100644
--- a/server/tests/utils/videos/videos.ts
+++ b/server/tests/utils/videos/videos.ts
@@ -123,6 +123,17 @@ function getVideosList (url: string) {
.expect('Content-Type', /json/)
}
+function getLocalVideos (url: string) {
+ const path = '/api/v1/videos'
+
+ return request(url)
+ .get(path)
+ .query({ sort: 'name', filter: 'local' })
+ .set('Accept', 'application/json')
+ .expect(200)
+ .expect('Content-Type', /json/)
+}
+
function getMyVideos (url: string, accessToken: string, start: number, count: number, sort?: string) {
const path = '/api/v1/users/me/videos'
@@ -487,6 +498,7 @@ export {
rateVideo,
viewVideo,
parseTorrentVideo,
+ getLocalVideos,
completeVideoCheck,
checkVideoFilesWereRemoved
}
diff --git a/shared/models/videos/video-query.type.ts b/shared/models/videos/video-query.type.ts
new file mode 100644
index 000000000..ff0f527f3
--- /dev/null
+++ b/shared/models/videos/video-query.type.ts
@@ -0,0 +1 @@
+export type VideoFilter = 'local'