diff options
Diffstat (limited to 'server/controllers')
-rw-r--r-- | server/controllers/api/videos/index.ts | 7 | ||||
-rw-r--r-- | server/controllers/download.ts | 78 | ||||
-rw-r--r-- | server/controllers/index.ts | 1 | ||||
-rw-r--r-- | server/controllers/lazy-static.ts | 23 | ||||
-rw-r--r-- | server/controllers/static.ts | 88 |
5 files changed, 105 insertions, 92 deletions
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 9504c40a4..dcd6194ae 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -7,7 +7,7 @@ import { changeVideoChannelShare } from '@server/lib/activitypub/share' | |||
7 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' | 7 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' |
8 | import { LiveManager } from '@server/lib/live-manager' | 8 | import { LiveManager } from '@server/lib/live-manager' |
9 | import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' | 9 | import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' |
10 | import { getVideoFilePath } from '@server/lib/video-paths' | 10 | import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' |
11 | import { getServerActor } from '@server/models/application/application' | 11 | import { getServerActor } from '@server/models/application/application' |
12 | import { MVideoFullLight } from '@server/types/models' | 12 | import { MVideoFullLight } from '@server/types/models' |
13 | import { VideoCreate, VideoState, VideoUpdate } from '../../../../shared' | 13 | import { VideoCreate, VideoState, VideoUpdate } from '../../../../shared' |
@@ -189,6 +189,7 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
189 | videoData.duration = videoPhysicalFile['duration'] // duration was added by a previous middleware | 189 | videoData.duration = videoPhysicalFile['duration'] // duration was added by a previous middleware |
190 | 190 | ||
191 | const video = new VideoModel(videoData) as MVideoFullLight | 191 | const video = new VideoModel(videoData) as MVideoFullLight |
192 | video.VideoChannel = res.locals.videoChannel | ||
192 | video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object | 193 | video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object |
193 | 194 | ||
194 | const videoFile = new VideoFileModel({ | 195 | const videoFile = new VideoFileModel({ |
@@ -205,6 +206,8 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
205 | videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).videoFileResolution | 206 | videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).videoFileResolution |
206 | } | 207 | } |
207 | 208 | ||
209 | videoFile.filename = generateVideoFilename(video, false, videoFile.resolution, videoFile.extname) | ||
210 | |||
208 | // Move physical file | 211 | // Move physical file |
209 | const destination = getVideoFilePath(video, videoFile) | 212 | const destination = getVideoFilePath(video, videoFile) |
210 | await move(videoPhysicalFile.path, destination) | 213 | await move(videoPhysicalFile.path, destination) |
@@ -219,7 +222,7 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
219 | }) | 222 | }) |
220 | 223 | ||
221 | // Create the torrent file | 224 | // Create the torrent file |
222 | await createTorrentAndSetInfoHash(video, videoFile) | 225 | await createTorrentAndSetInfoHash(video, video, videoFile) |
223 | 226 | ||
224 | const { videoCreated } = await sequelizeTypescript.transaction(async t => { | 227 | const { videoCreated } = await sequelizeTypescript.transaction(async t => { |
225 | const sequelizeOptions = { transaction: t } | 228 | const sequelizeOptions = { transaction: t } |
diff --git a/server/controllers/download.ts b/server/controllers/download.ts new file mode 100644 index 000000000..27caa1518 --- /dev/null +++ b/server/controllers/download.ts | |||
@@ -0,0 +1,78 @@ | |||
1 | import * as cors from 'cors' | ||
2 | import * as express from 'express' | ||
3 | import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' | ||
4 | import { getVideoFilePath } from '@server/lib/video-paths' | ||
5 | import { MVideoFile, MVideoFullLight } from '@server/types/models' | ||
6 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
7 | import { VideoStreamingPlaylistType } from '@shared/models' | ||
8 | import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants' | ||
9 | import { asyncMiddleware, videosDownloadValidator } from '../middlewares' | ||
10 | |||
11 | const downloadRouter = express.Router() | ||
12 | |||
13 | downloadRouter.use(cors()) | ||
14 | |||
15 | downloadRouter.use( | ||
16 | STATIC_DOWNLOAD_PATHS.TORRENTS + ':filename', | ||
17 | downloadTorrent | ||
18 | ) | ||
19 | |||
20 | downloadRouter.use( | ||
21 | STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension', | ||
22 | asyncMiddleware(videosDownloadValidator), | ||
23 | downloadVideoFile | ||
24 | ) | ||
25 | |||
26 | downloadRouter.use( | ||
27 | STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension', | ||
28 | asyncMiddleware(videosDownloadValidator), | ||
29 | downloadHLSVideoFile | ||
30 | ) | ||
31 | |||
32 | // --------------------------------------------------------------------------- | ||
33 | |||
34 | export { | ||
35 | downloadRouter | ||
36 | } | ||
37 | |||
38 | // --------------------------------------------------------------------------- | ||
39 | |||
40 | async function downloadTorrent (req: express.Request, res: express.Response) { | ||
41 | const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) | ||
42 | if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | ||
43 | |||
44 | return res.download(result.path, result.downloadName) | ||
45 | } | ||
46 | |||
47 | function downloadVideoFile (req: express.Request, res: express.Response) { | ||
48 | const video = res.locals.videoAll | ||
49 | |||
50 | const videoFile = getVideoFile(req, video.VideoFiles) | ||
51 | if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() | ||
52 | |||
53 | return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`) | ||
54 | } | ||
55 | |||
56 | function downloadHLSVideoFile (req: express.Request, res: express.Response) { | ||
57 | const video = res.locals.videoAll | ||
58 | const playlist = getHLSPlaylist(video) | ||
59 | if (!playlist) return res.status(HttpStatusCode.NOT_FOUND_404).end | ||
60 | |||
61 | const videoFile = getVideoFile(req, playlist.VideoFiles) | ||
62 | if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() | ||
63 | |||
64 | const filename = `${video.name}-${videoFile.resolution}p-${playlist.getStringType()}${videoFile.extname}` | ||
65 | return res.download(getVideoFilePath(playlist, videoFile), filename) | ||
66 | } | ||
67 | |||
68 | function getVideoFile (req: express.Request, files: MVideoFile[]) { | ||
69 | const resolution = parseInt(req.params.resolution, 10) | ||
70 | return files.find(f => f.resolution === resolution) | ||
71 | } | ||
72 | |||
73 | function getHLSPlaylist (video: MVideoFullLight) { | ||
74 | const playlist = video.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | ||
75 | if (!playlist) return undefined | ||
76 | |||
77 | return Object.assign(playlist, { Video: video }) | ||
78 | } | ||
diff --git a/server/controllers/index.ts b/server/controllers/index.ts index 5a199ae9c..fa27ecec2 100644 --- a/server/controllers/index.ts +++ b/server/controllers/index.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | export * from './activitypub' | 1 | export * from './activitypub' |
2 | export * from './api' | 2 | export * from './api' |
3 | export * from './client' | 3 | export * from './client' |
4 | export * from './download' | ||
4 | export * from './feeds' | 5 | export * from './feeds' |
5 | export * from './services' | 6 | export * from './services' |
6 | export * from './static' | 7 | export * from './static' |
diff --git a/server/controllers/lazy-static.ts b/server/controllers/lazy-static.ts index 656dea223..c2f5c7b56 100644 --- a/server/controllers/lazy-static.ts +++ b/server/controllers/lazy-static.ts | |||
@@ -1,12 +1,13 @@ | |||
1 | import * as cors from 'cors' | 1 | import * as cors from 'cors' |
2 | import * as express from 'express' | 2 | import * as express from 'express' |
3 | import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' | ||
4 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | ||
5 | import { logger } from '../helpers/logger' | ||
3 | import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' | 6 | import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' |
7 | import { avatarPathUnsafeCache, pushAvatarProcessInQueue } from '../lib/avatar' | ||
4 | import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache' | 8 | import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache' |
5 | import { asyncMiddleware } from '../middlewares' | 9 | import { asyncMiddleware } from '../middlewares' |
6 | import { AvatarModel } from '../models/avatar/avatar' | 10 | import { AvatarModel } from '../models/avatar/avatar' |
7 | import { logger } from '../helpers/logger' | ||
8 | import { avatarPathUnsafeCache, pushAvatarProcessInQueue } from '../lib/avatar' | ||
9 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | ||
10 | 11 | ||
11 | const lazyStaticRouter = express.Router() | 12 | const lazyStaticRouter = express.Router() |
12 | 13 | ||
@@ -27,6 +28,11 @@ lazyStaticRouter.use( | |||
27 | asyncMiddleware(getVideoCaption) | 28 | asyncMiddleware(getVideoCaption) |
28 | ) | 29 | ) |
29 | 30 | ||
31 | lazyStaticRouter.use( | ||
32 | LAZY_STATIC_PATHS.TORRENTS + ':filename', | ||
33 | asyncMiddleware(getTorrent) | ||
34 | ) | ||
35 | |||
30 | // --------------------------------------------------------------------------- | 36 | // --------------------------------------------------------------------------- |
31 | 37 | ||
32 | export { | 38 | export { |
@@ -67,19 +73,26 @@ async function getAvatar (req: express.Request, res: express.Response) { | |||
67 | const path = avatar.getPath() | 73 | const path = avatar.getPath() |
68 | 74 | ||
69 | avatarPathUnsafeCache.set(filename, path) | 75 | avatarPathUnsafeCache.set(filename, path) |
70 | return res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER }) | 76 | return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) |
71 | } | 77 | } |
72 | 78 | ||
73 | async function getPreview (req: express.Request, res: express.Response) { | 79 | async function getPreview (req: express.Request, res: express.Response) { |
74 | const result = await VideosPreviewCache.Instance.getFilePath(req.params.filename) | 80 | const result = await VideosPreviewCache.Instance.getFilePath(req.params.filename) |
75 | if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 81 | if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) |
76 | 82 | ||
77 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.SERVER }) | 83 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) |
78 | } | 84 | } |
79 | 85 | ||
80 | async function getVideoCaption (req: express.Request, res: express.Response) { | 86 | async function getVideoCaption (req: express.Request, res: express.Response) { |
81 | const result = await VideosCaptionCache.Instance.getFilePath(req.params.filename) | 87 | const result = await VideosCaptionCache.Instance.getFilePath(req.params.filename) |
82 | if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 88 | if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) |
83 | 89 | ||
90 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) | ||
91 | } | ||
92 | |||
93 | async function getTorrent (req: express.Request, res: express.Response) { | ||
94 | const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) | ||
95 | if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | ||
96 | |||
84 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.SERVER }) | 97 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.SERVER }) |
85 | } | 98 | } |
diff --git a/server/controllers/static.ts b/server/controllers/static.ts index 2064857eb..7cc7f2c62 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts | |||
@@ -3,10 +3,7 @@ import * as express from 'express' | |||
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { getRegisteredPlugins, getRegisteredThemes } from '@server/controllers/api/config' | 4 | import { getRegisteredPlugins, getRegisteredThemes } from '@server/controllers/api/config' |
5 | import { serveIndexHTML } from '@server/lib/client-html' | 5 | import { serveIndexHTML } from '@server/lib/client-html' |
6 | import { getTorrentFilePath, getVideoFilePath } from '@server/lib/video-paths' | ||
7 | import { MVideoFile, MVideoFullLight } from '@server/types/models' | ||
8 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | 6 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' |
9 | import { VideoStreamingPlaylistType } from '@shared/models/videos/video-streaming-playlist.type' | ||
10 | import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo' | 7 | import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo' |
11 | import { root } from '../helpers/core-utils' | 8 | import { root } from '../helpers/core-utils' |
12 | import { CONFIG, isEmailEnabled } from '../initializers/config' | 9 | import { CONFIG, isEmailEnabled } from '../initializers/config' |
@@ -16,14 +13,13 @@ import { | |||
16 | HLS_STREAMING_PLAYLIST_DIRECTORY, | 13 | HLS_STREAMING_PLAYLIST_DIRECTORY, |
17 | PEERTUBE_VERSION, | 14 | PEERTUBE_VERSION, |
18 | ROUTE_CACHE_LIFETIME, | 15 | ROUTE_CACHE_LIFETIME, |
19 | STATIC_DOWNLOAD_PATHS, | ||
20 | STATIC_MAX_AGE, | 16 | STATIC_MAX_AGE, |
21 | STATIC_PATHS, | 17 | STATIC_PATHS, |
22 | WEBSERVER | 18 | WEBSERVER |
23 | } from '../initializers/constants' | 19 | } from '../initializers/constants' |
24 | import { getThemeOrDefault } from '../lib/plugins/theme-utils' | 20 | import { getThemeOrDefault } from '../lib/plugins/theme-utils' |
25 | import { getEnabledResolutions } from '../lib/video-transcoding' | 21 | import { getEnabledResolutions } from '../lib/video-transcoding' |
26 | import { asyncMiddleware, videosDownloadValidator } from '../middlewares' | 22 | import { asyncMiddleware } from '../middlewares' |
27 | import { cacheRoute } from '../middlewares/cache' | 23 | import { cacheRoute } from '../middlewares/cache' |
28 | import { UserModel } from '../models/account/user' | 24 | import { UserModel } from '../models/account/user' |
29 | import { VideoModel } from '../models/video/video' | 25 | import { VideoModel } from '../models/video/video' |
@@ -37,47 +33,23 @@ staticRouter.use(cors()) | |||
37 | Cors is very important to let other servers access torrent and video files | 33 | Cors is very important to let other servers access torrent and video files |
38 | */ | 34 | */ |
39 | 35 | ||
36 | // FIXME: deprecated in 3.2, use lazy-statics instead | ||
40 | const torrentsPhysicalPath = CONFIG.STORAGE.TORRENTS_DIR | 37 | const torrentsPhysicalPath = CONFIG.STORAGE.TORRENTS_DIR |
41 | staticRouter.use( | 38 | staticRouter.use( |
42 | STATIC_PATHS.TORRENTS, | 39 | STATIC_PATHS.TORRENTS, |
43 | cors(), | ||
44 | express.static(torrentsPhysicalPath, { maxAge: 0 }) // Don't cache because we could regenerate the torrent file | 40 | express.static(torrentsPhysicalPath, { maxAge: 0 }) // Don't cache because we could regenerate the torrent file |
45 | ) | 41 | ) |
46 | staticRouter.use( | ||
47 | STATIC_DOWNLOAD_PATHS.TORRENTS + ':id-:resolution([0-9]+).torrent', | ||
48 | asyncMiddleware(videosDownloadValidator), | ||
49 | downloadTorrent | ||
50 | ) | ||
51 | staticRouter.use( | ||
52 | STATIC_DOWNLOAD_PATHS.TORRENTS + ':id-:resolution([0-9]+)-hls.torrent', | ||
53 | asyncMiddleware(videosDownloadValidator), | ||
54 | downloadHLSVideoFileTorrent | ||
55 | ) | ||
56 | 42 | ||
57 | // Videos path for webseeding | 43 | // Videos path for webseed |
58 | staticRouter.use( | 44 | staticRouter.use( |
59 | STATIC_PATHS.WEBSEED, | 45 | STATIC_PATHS.WEBSEED, |
60 | cors(), | ||
61 | express.static(CONFIG.STORAGE.VIDEOS_DIR, { fallthrough: false }) // 404 because we don't have this video | 46 | express.static(CONFIG.STORAGE.VIDEOS_DIR, { fallthrough: false }) // 404 because we don't have this video |
62 | ) | 47 | ) |
63 | staticRouter.use( | 48 | staticRouter.use( |
64 | STATIC_PATHS.REDUNDANCY, | 49 | STATIC_PATHS.REDUNDANCY, |
65 | cors(), | ||
66 | express.static(CONFIG.STORAGE.REDUNDANCY_DIR, { fallthrough: false }) // 404 because we don't have this video | 50 | express.static(CONFIG.STORAGE.REDUNDANCY_DIR, { fallthrough: false }) // 404 because we don't have this video |
67 | ) | 51 | ) |
68 | 52 | ||
69 | staticRouter.use( | ||
70 | STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension', | ||
71 | asyncMiddleware(videosDownloadValidator), | ||
72 | downloadVideoFile | ||
73 | ) | ||
74 | |||
75 | staticRouter.use( | ||
76 | STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension', | ||
77 | asyncMiddleware(videosDownloadValidator), | ||
78 | downloadHLSVideoFile | ||
79 | ) | ||
80 | |||
81 | // HLS | 53 | // HLS |
82 | staticRouter.use( | 54 | staticRouter.use( |
83 | STATIC_PATHS.STREAMING_PLAYLISTS.HLS, | 55 | STATIC_PATHS.STREAMING_PLAYLISTS.HLS, |
@@ -327,60 +299,6 @@ async function generateNodeinfo (req: express.Request, res: express.Response) { | |||
327 | return res.send(json).end() | 299 | return res.send(json).end() |
328 | } | 300 | } |
329 | 301 | ||
330 | function downloadTorrent (req: express.Request, res: express.Response) { | ||
331 | const video = res.locals.videoAll | ||
332 | |||
333 | const videoFile = getVideoFile(req, video.VideoFiles) | ||
334 | if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() | ||
335 | |||
336 | return res.download(getTorrentFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p.torrent`) | ||
337 | } | ||
338 | |||
339 | function downloadHLSVideoFileTorrent (req: express.Request, res: express.Response) { | ||
340 | const video = res.locals.videoAll | ||
341 | |||
342 | const playlist = getHLSPlaylist(video) | ||
343 | if (!playlist) return res.status(HttpStatusCode.NOT_FOUND_404).end | ||
344 | |||
345 | const videoFile = getVideoFile(req, playlist.VideoFiles) | ||
346 | if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() | ||
347 | |||
348 | return res.download(getTorrentFilePath(playlist, videoFile), `${video.name}-${videoFile.resolution}p-hls.torrent`) | ||
349 | } | ||
350 | |||
351 | function downloadVideoFile (req: express.Request, res: express.Response) { | ||
352 | const video = res.locals.videoAll | ||
353 | |||
354 | const videoFile = getVideoFile(req, video.VideoFiles) | ||
355 | if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() | ||
356 | |||
357 | return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`) | ||
358 | } | ||
359 | |||
360 | function downloadHLSVideoFile (req: express.Request, res: express.Response) { | ||
361 | const video = res.locals.videoAll | ||
362 | const playlist = getHLSPlaylist(video) | ||
363 | if (!playlist) return res.status(HttpStatusCode.NOT_FOUND_404).end | ||
364 | |||
365 | const videoFile = getVideoFile(req, playlist.VideoFiles) | ||
366 | if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() | ||
367 | |||
368 | const filename = `${video.name}-${videoFile.resolution}p-${playlist.getStringType()}${videoFile.extname}` | ||
369 | return res.download(getVideoFilePath(playlist, videoFile), filename) | ||
370 | } | ||
371 | |||
372 | function getVideoFile (req: express.Request, files: MVideoFile[]) { | ||
373 | const resolution = parseInt(req.params.resolution, 10) | ||
374 | return files.find(f => f.resolution === resolution) | ||
375 | } | ||
376 | |||
377 | function getHLSPlaylist (video: MVideoFullLight) { | ||
378 | const playlist = video.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | ||
379 | if (!playlist) return undefined | ||
380 | |||
381 | return Object.assign(playlist, { Video: video }) | ||
382 | } | ||
383 | |||
384 | function getCup (req: express.Request, res: express.Response, next: express.NextFunction) { | 302 | function getCup (req: express.Request, res: express.Response, next: express.NextFunction) { |
385 | res.status(HttpStatusCode.I_AM_A_TEAPOT_418) | 303 | res.status(HttpStatusCode.I_AM_A_TEAPOT_418) |
386 | res.setHeader('Accept-Additions', 'Non-Dairy;1,Sugar;1') | 304 | res.setHeader('Accept-Additions', 'Non-Dairy;1,Sugar;1') |