Compare commits

...

2 commits

4 changed files with 236 additions and 8 deletions

View file

@ -0,0 +1,135 @@
/*Canopy - The next generation of stoner streaming software
Copyright (C) 2024-2025 Rainbownapkin and the TTN Community
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.*/
//Local Imports
const media = require('./media');
const queuedMedia = require('./queuedMedia');
/**
* Class extending media which represents a queued piece of media
* @extends media
*/
class archivedMedia extends queuedMedia{
/**
* Creates a new queued media object
* @param {String} channel - Channel where object was queued
*/
constructor(title, fileName, url, id, type, duration, rawLink, startTime, startTimeStamp = 0, earlyEnd, uuid, channel){
//Call derived constructor
super(title, fileName, url, id, type, duration, rawLink, startTime, startTimeStamp, earlyEnd, uuid);
/**
* Channel media was queued
*/
this.channel = channel;
/**
* Media status type
*/
this.status = 'archived';
}
//statics
/**
* Creates a archivedMedia object from a media object
* @param {String} channel - Channel where object was queued
* @returns {archivedMedia} queuedMedia object created from given media object
*/
static fromMedia(media, startTime, startTimeStamp, channel){
//Create and return queuedMedia object from given media object and arguments
return new this(
media.title,
media.fileName,
media.url,
media.id,
media.type,
media.duration,
media.rawLink,
startTime,
startTimeStamp,
null,
null,
channel);
}
/**
* Creates a archivedMedia object from a queuedMedia object
* @param {String} channel - Channel where object was queued
* @returns {archivedMedia} queuedMedia object created from given media object
*/
static fromQueuedMedia(media, channel){
//Create and return queuedMedia object from given media object and arguments
return new this(
media.title,
media.fileName,
media.url,
media.id,
media.type,
media.duration,
media.rawLink,
media.startTime,
media.startTimeStamp,
media.earlyEnd,
null,
channel);
}
/**
* Converts array of media objects into array of archivedMedia objects
* @param {String} channel - Channel where object was queued
* @returns Array of converted queued media objects
*/
static fromMediaArray(mediaList, start, channel){
//Queued Media List
const archivedMediaList = [];
//Start Time Offset
let startOffset = 0;
for(let media of mediaList){
//Convert mediaObj to queuedMedia and push to the back of the list
archivedMediaList.push(this.fromMedia(media, start + startOffset, 0, channel));
//Set start offset to end of the current item
startOffset += (media.duration * 1000) + 5;
}
return archivedMediaList;
}
//methods
/**
* Generates a unique clone of a given media object
* @returns unique clone of media object
*/
clone(){
return new archivedMedia(
this.title,
this.fileName,
this.url,
this.id,
this.type,
this.duration,
this.rawLink,
this.startTime,
this.startTimeStamp,
this.earlyEnd,
null,
this.channel
);
}
}
module.exports = archivedMedia;

View file

@ -20,10 +20,12 @@ const validator = require('validator');
//Local imports //Local imports
const config = require('../../../../config.json'); const config = require('../../../../config.json');
const queuedMedia = require('./queuedMedia'); const queuedMedia = require('./queuedMedia');
const archivedMedia = require('./archivedMedia');
const yanker = require('../../../utils/media/yanker'); const yanker = require('../../../utils/media/yanker');
const loggerUtils = require('../../../utils/loggerUtils'); const loggerUtils = require('../../../utils/loggerUtils');
const channelModel = require('../../../schemas/channel/channelSchema'); const channelModel = require('../../../schemas/channel/channelSchema');
const permissionModel = require('../../../schemas/permissionSchema'); const permissionModel = require('../../../schemas/permissionSchema');
const archivedMediaModel = require('../../../schemas/channel/media/archivedMediaSchema');
/** /**
* Object represneting a single channel's media queue * Object represneting a single channel's media queue
@ -767,7 +769,7 @@ class queue{
//Get requested media //Get requested media
const media = this.getItemByUUID(uuid); const media = this.getItemByUUID(uuid);
//If we got a bad request //If we couldn't find anything in the current channel schedule
if(media == null){ if(media == null){
try{ try{
//If we wheren't handed a channel //If we wheren't handed a channel
@ -1145,7 +1147,7 @@ class queue{
//If nowPlaying isn't null and isn't what we're about to throw on //If nowPlaying isn't null and isn't what we're about to throw on
if(chanDB.media.nowPlaying != null && chanDB.media.nowPlaying.uuid.toString() != mediaObj.uuid){ if(chanDB.media.nowPlaying != null && chanDB.media.nowPlaying.uuid.toString() != mediaObj.uuid){
//Archive whats already in there since we're about to clobber the fuck out of it //Archive whats already in there since we're about to clobber the fuck out of it
chanDB.media.archived.push(chanDB.media.nowPlaying); this.archiveMedia(chanDB.media.nowPlaying);
} }
//Set the now playing queued media document //Set the now playing queued media document
@ -1284,7 +1286,7 @@ class queue{
//If archiving is enabled //If archiving is enabled
if(!noArchive){ if(!noArchive){
//Add the item to the channel archive //Add the item to the channel archive
chanDB.media.archived.push(wasPlaying); this.archiveMedia(wasPlaying);
} }
//broadcast queue using unsaved archive, run this before chanDB.save() for better responsiveness //broadcast queue using unsaved archive, run this before chanDB.save() for better responsiveness
@ -1383,7 +1385,7 @@ class queue{
let finished = false; let finished = false;
//Throw the livestream into the archive //Throw the livestream into the archive
chanDB.media.archived.push(wasPlaying); this.archiveMedia(wasPlaying);
//Save the DB //Save the DB
await chanDB.save(); await chanDB.save();
@ -1468,7 +1470,7 @@ class queue{
} }
//Throw the livestream into the archive //Throw the livestream into the archive
chanDB.media.archived.push(wasPlaying); this.archiveMedia(wasPlaying);
//Set the current place to schedule items at 5ms after the end of the live stream //Set the current place to schedule items at 5ms after the end of the live stream
let curPlace = wasPlaying.getEndTime() + 5; let curPlace = wasPlaying.getEndTime() + 5;
@ -1810,7 +1812,7 @@ class queue{
chanDB.media.nowPlaying = null; chanDB.media.nowPlaying = null;
//Archive the bitch //Archive the bitch
chanDB.media.archived.push(wasPlaying); this.archiveMedia(wasPlaying);
} }
} }
@ -1840,7 +1842,7 @@ class queue{
//If it's been ended //If it's been ended
}else{ }else{
//Archive ended media //Archive ended media
chanDB.media.archived.push(record); this.archiveMedia(record);
} }
} }
} }
@ -1892,6 +1894,17 @@ class queue{
loggerUtils.localExceptionHandler(err); loggerUtils.localExceptionHandler(err);
} }
} }
/**
* Commits a queuedMedia object to the media archives
* @param {queuedMedia} media - Media object to be archived
*/
async archiveMedia(media){
//Convert queuedMedia object to archivedMedia object
let archived = archivedMedia.fromQueuedMedia(media, this.channel.name);
//Save archivedMedia object to site-wide media archive (gross but performant)
let archivedDoc = await archivedMediaModel.create(archived);
}
} }
module.exports = queue; module.exports = queue;

View file

@ -21,7 +21,7 @@ const loggerUtils = require("../../../utils/loggerUtils");
const channelModel = require("../../../schemas/channel/channelSchema"); const channelModel = require("../../../schemas/channel/channelSchema");
/** /**
* Class containg global server-side private message relay logic * Class containg per-channel server-side queue broadcasting logic
* *
* Exists to make broadcasting channel queues to groups of authenticated users with the 'read-queue' perm as painless as possible, * Exists to make broadcasting channel queues to groups of authenticated users with the 'read-queue' perm as painless as possible,
* reducing DB call/perm checks to just connection time, and not requireing any out-of-library user iteration at broadcast time. * reducing DB call/perm checks to just connection time, and not requireing any out-of-library user iteration at broadcast time.

View file

@ -0,0 +1,80 @@
/*Canopy - The next generation of stoner streaming software
Copyright (C) 2024-2025 Rainbownapkin and the TTN Community
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.*/
//NPM Imports
const {mongoose} = require('mongoose');
//Local Imports
const mediaSchema = require('./mediaSchema');
const archivedMedia = require('../../../app/channel/media/archivedMedia');
/**
* DB Schema for documents representing a queued media object
*/
const archivedProperties = new mongoose.Schema({
channel: {
type: mongoose.SchemaTypes.String,
required: true,
},
startTime: {
type: mongoose.SchemaTypes.Number,
required: true,
},
startTimeStamp: {
type: mongoose.SchemaTypes.Number,
required: false,
},
earlyEnd: {
type: mongoose.SchemaTypes.Number,
required: false,
},
uuid: {
type: mongoose.SchemaTypes.UUID,
required: true,
},
},
{
discriminatorKey: 'status'
});
//Methods
/**
* Rehydrate to a full phat archived media object
* @returns {archivedMedia} A full phat archived media object, re-hydrated from the DB
*/
archivedProperties.methods.rehydrate = function(){
return new archivedMedia(
this.title,
this.fileName,
this.url,
this.id,
this.type,
this.duration,
//We don't save raw links that are stored seperate from the standard URL as they tend to expire.
undefined,
this.startTime,
this.startTimeStamp,
this.earlyEnd,
this.uuid.toString(),
this.channel
);
}
//Create 'archivedMediaSchema' as descriminator of mediaSchema
var archivedMediaSchema = mediaSchema.discriminator('archived', archivedProperties);
//Export mongoose model based on archivedSchema
module.exports = mongoose.model("archivedMedia", archivedMediaSchema);