diff --git a/backend/controllers/events.js b/backend/controllers/events.js index 3717638..f54cabe 100644 --- a/backend/controllers/events.js +++ b/backend/controllers/events.js @@ -4,6 +4,7 @@ const https = require('https'); var db = require('../db'); var mongoose = require("mongoose"); var Event = require("../models/event"); +var Podcast = require("../models/podcast"); var System = require("../models/system"); var Talkgroup = require("../models/talkgroup"); const AdmZip = require("adm-zip"); @@ -104,7 +105,7 @@ const createEventZip = async (folder, filename, zipFolder) => { } -const uploadEventZip = (zipFile, s3File) => { +const uploadFile = (zipFile, s3File) => { var s3Src = fs.createReadStream(zipFile); return new Promise((resolve, reject) => { const s3Params = { @@ -137,7 +138,7 @@ const ffmpeg = async (command) => { console.error(data.toString()); //I'm not sure what debug is }); ffmpeg.stdout.on("data", (data) => { - console.log(data.toString()); //I'm not sure what debug is + //console.log(data.toString()); //I'm not sure what debug is }); ffmpeg.on("exit", (code, signal) => { if (signal) @@ -229,8 +230,21 @@ const cleanupEvent = (folder, filename) => { fs.rmSync(filename); } +const savePodcast = (event, podcastUrl) => { + let podcast = new Podcast(event.toObject()); + podcast.downloadUrl = podcastUrl; + let totalTime = 0; + event.calls.forEach((call) => { + totalTime = totalTime + call.len; + if (podcast.systems.indexOf(call.systemName) === -1) { + podcast.systems.push(call.systemName); + } + }); + podcast.len = totalTime; + podcast.save(); +} + const packageEvent = (eventId) => { - const event = "tst" const tmpdir = os.tmpdir(); return new Promise(async (resolve, reject) => { var objectId = eventId; @@ -262,14 +276,15 @@ const packageEvent = (eventId) => { exportEventJson(tmpEventFolder, event); createEventZip(tmpEventFolder, zipFile, eventFolder); - await uploadEventZip(zipFile, s3ZipFile); + await uploadFile(zipFile, s3ZipFile); var downloadUrl = 'https://' + s3_endpoint + "/" + s3_bucket + "/" + s3ZipFile; var podcastUrl = 'https://' + s3_endpoint + "/" + s3_bucket + "/" + s3PodcastFile; event.downloadUrl = downloadUrl; await event.save(); await createPodcast(tmpEventFolder, podcastFile, event); - await uploadEventZip(podcastFile,s3PodcastFile); + await uploadFile(podcastFile,podcastUrl); + await savePodcast(event,podcastUrl); console.log("uploaded to: " + podcastUrl); @@ -283,7 +298,6 @@ const packageEvent = (eventId) => { }); }; - const compareCalls = (a, b) => { const aTimestamp = new Date(a.time).getTime() const bTimestamp = new Date(b.time).getTime() diff --git a/backend/models/podcast.js b/backend/models/podcast.js new file mode 100644 index 0000000..5a2479a --- /dev/null +++ b/backend/models/podcast.js @@ -0,0 +1,21 @@ +var mongoose = require('mongoose'); + + + +var podcastSchema = mongoose.Schema({ + title: String, + description: String, + expireTime: Date, + startTime: Date, + endTime: Date, + downloadUrl: String, + systems: [String], + numCalls: Number, + len: Number +}, +{ + timestamps: true +}); + + +module.exports = mongoose.model('Podcast', podcastSchema); diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 28a5624..158cde7 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,5 +1,5 @@ # build environment -FROM node:13.12.0-alpine as build +FROM node:19-alpine3.16 as build WORKDIR /app ENV PATH /app/node_modules/.bin:$PATH ARG REACT_APP_BACKEND_SERVER @@ -13,7 +13,7 @@ COPY . ./ RUN npm run build # production environment -FROM node:13.12.0-alpine +FROM node:19-alpine3.16 RUN mkdir -p /app/public COPY ./package.json /tmp RUN cd /tmp && npm install diff --git a/frontend/package.json b/frontend/package.json index 5298212..0a6781b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,16 +4,17 @@ "private": true, "dependencies": { "@artsy/fresnel": "6.1.0", + "@lagunovsky/redux-react-router": "^4.3.0", "@reduxjs/toolkit": "^1.9.1", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^14.4.3", "axios": "1.2.1", - "connected-react-router": "^6.9.3", "date-fns": "2.29.3", "ejs": "3.1.8", "history": "^5.3.0", "mongodb": "^4.13.0", + "podcast": "^2.0.1", "react": "^18.2.0", "react-audio-player": "0.17.0", "react-datepicker": "4.8.0", diff --git a/frontend/public/podcast/channel_icon.png b/frontend/public/podcast/channel_icon.png new file mode 100644 index 0000000..547f4be Binary files /dev/null and b/frontend/public/podcast/channel_icon.png differ diff --git a/frontend/public/podcast/cover.png b/frontend/public/podcast/cover.png new file mode 100644 index 0000000..259769d Binary files /dev/null and b/frontend/public/podcast/cover.png differ diff --git a/frontend/server/index.js b/frontend/server/index.js index c4cb72a..8b7d082 100644 --- a/frontend/server/index.js +++ b/frontend/server/index.js @@ -4,7 +4,7 @@ const express = require("express"); const path = require("path"); const secrets = require("./config/secrets"); const configureExpress = require("./config/express"); - +const { Podcast,Item,FeedOptions } = require('podcast'); // ------------------------------------------- @@ -26,12 +26,6 @@ db.connect(function (err) { const isDev = process.env.NODE_ENV === "development" - - - - - - // ------------------------------------------- @@ -115,6 +109,59 @@ exports.get_card = function(req, res) { } */ + +async function getPodcast(req, res, next) { + const now = new Date(); + try { + const podcastsCollection = db.get().collection('podcasts'); + podcastsCollection.find().toArray(function(err, podcasts) { + const feed = new Podcast({ + title: 'OpenMHz', + description: 'Real public safety radio recordings, from across the U.S - Interesting events, curated by the OpenMHz community.', + feedUrl: 'https://openmhz.com/rss.xml', + siteUrl: 'https://openmhz.com', + imageUrl: 'https://openmhz.com/podcast/cover.png', + author: 'OpenMHz', + copyright: '© 2022 Robotastic', + language: 'en', + pubDate: 'May 20, 2012 04:00:00 GMT', + ttl: 60, + itunesAuthor: 'OpenMHz', + itunesSubtitle: 'I am a sub title', + itunesSummary: 'Real public safety radio recordings, from across the U.S - Interesting events, curated by the OpenMHz community.', + itunesOwner: { name: 'Luke Berndt', email: 'luke@robotastic.com' }, + itunesExplicit: false, + itunesCategory: [{ + text: 'Government' + }, { text: "Daily News"}, {text: "True Crime"}], + itunesImage: 'https://openmhz.com/podcast/cover.png' + }); + for (const podcast of podcasts) { + feed.addItem({ + title: podcast.title, + description: podcast.description, + enclosure: { + url: podcast.downloadUrl, // link to the item + }, + author: 'OpenMHz', // optional - defaults to feed author property + date: podcast.startTime, // any format that js Date can parse. + itunesAuthor: 'OpenMHz', + itunesExplicit: false, + itunesSummary: podcast.description, + itunesDuration: podcast.len + }); + } + const xml = feed.buildXml(); + res.set('Content-Type', 'text/xml'); + res.send(xml); + }); + + } catch (err) { + console.error(err); + return; + } +} + async function getCalls(req, res, next) { if (req.query && req.query["call-id"]) { @@ -186,6 +233,9 @@ app.use(express.static(path.join(__dirname, "public"))); //app.get("/system/:shortName", getCalls) //app.get("/cards/:id", getCard) + +app.get("/rss.xml", getPodcast); + app.get("*", (req, res, next) => { res.sendFile(__dirname + '/public/index.html'); }); diff --git a/frontend/src/redux-router/configureStore.js b/frontend/src/redux-router/configureStore.js index 434bfe7..9e67bfb 100644 --- a/frontend/src/redux-router/configureStore.js +++ b/frontend/src/redux-router/configureStore.js @@ -3,7 +3,8 @@ import { createBrowserHistory } from 'history' import { applyMiddleware, compose, createStore } from 'redux' -import { routerMiddleware } from 'connected-react-router' +import { createRouterMiddleware } from '@lagunovsky/redux-react-router' +//import { routerMiddleware } from 'connected-react-router' import createRootReducer from './reducers' import { apiSlice } from '../features/api/apiSlice' @@ -18,7 +19,7 @@ export default function configureStore(preloadedState) { preloadedState, composeEnhancers( applyMiddleware( - routerMiddleware(history), // for dispatching history actions + createRouterMiddleware(history), // for dispatching history actions thunk, apiSlice.middleware, // ... other middlewares ... diff --git a/frontend/src/redux-router/reducers.js b/frontend/src/redux-router/reducers.js index cd105a9..f7e84f9 100644 --- a/frontend/src/redux-router/reducers.js +++ b/frontend/src/redux-router/reducers.js @@ -1,13 +1,14 @@ // from https://github.com/supasate/connected-react-router import { combineReducers } from 'redux' -import { connectRouter } from 'connected-react-router' +//import { connectRouter } from 'connected-react-router' +import { createRouterReducer } from '@lagunovsky/redux-react-router' import {apiSlice} from "../features/api/apiSlice" import callPlayerSlice from "../features/callPlayer/callPlayerSlice" import { callsReducer } from "../features/calls/callsSlice" export default (history) => combineReducers({ - router: connectRouter(history), + router: createRouterReducer(history), callPlayer: callPlayerSlice, calls: callsReducer, [apiSlice.reducerPath]: apiSlice.reducer, diff --git a/frontend/yarn.lock b/frontend/yarn.lock index f55eb73..165a2e0 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2293,6 +2293,11 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@lagunovsky/redux-react-router@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@lagunovsky/redux-react-router/-/redux-react-router-4.3.0.tgz#2969158850b6f304f6af59de966a8e5589e2d2e7" + integrity sha512-zjjIu0xiX4cAE6O0ttLwo/Fk2zF3oeDfP3nC9YPUFSMnK7QumxS4xQIaH8x+amIIrg3klvPAk64L1bZLvDHl5w== + "@leichtgewicht/ip-codec@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" @@ -4059,17 +4064,6 @@ connect-history-api-fallback@^2.0.0: resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== -connected-react-router@^6.9.3: - version "6.9.3" - resolved "https://registry.yarnpkg.com/connected-react-router/-/connected-react-router-6.9.3.tgz#72300aca9f9d6f38e1f4a2901572faa02adec972" - integrity sha512-4ThxysOiv/R2Dc4Cke1eJwjKwH1Y51VDwlOrOfs1LjpdYOVvCNjNkZDayo7+sx42EeGJPQUNchWkjAIJdXGIOQ== - dependencies: - lodash.isequalwith "^4.4.0" - prop-types "^15.7.2" - optionalDependencies: - immutable "^3.8.1 || ^4.0.0" - seamless-immutable "^7.1.3" - content-disposition@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" @@ -5859,11 +5853,6 @@ immer@^9.0.16, immer@^9.0.7: resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.16.tgz#8e7caab80118c2b54b37ad43e05758cdefad0198" integrity sha512-qenGE7CstVm1NrHQbMh8YaSzTZTFNP3zPqr3YU0S0UY441j4bJTg4A2Hh5KAhwgaiU6ZZ1Ar6y/2f4TblnMReQ== -"immutable@^3.8.1 || ^4.0.0": - version "4.2.1" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.2.1.tgz#8a4025691018c560a40c67e43d698f816edc44d4" - integrity sha512-7WYV7Q5BTs0nlQm7tl92rDYYoyELLKHoDMBKhrxEoiV4mrfVdRz8hzPiYOzH7yWjzoVEamxRuAqhxL2PLRwZYQ== - import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -7004,11 +6993,6 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== -lodash.isequalwith@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.isequalwith/-/lodash.isequalwith-4.4.0.tgz#266726ddd528f854f21f4ea98a065606e0fbc6b0" - integrity sha512-dcZON0IalGBpRmJBmMkaoV7d3I80R2O+FrzsZyHdNSFrANq/cgDqKQNmAHE8UEj4+QYWwwhkQOVdLHiAopzlsQ== - lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -7148,6 +7132,18 @@ mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== +mime-db@~1.25.0: + version "1.25.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.25.0.tgz#c18dbd7c73a5dbf6f44a024dc0d165a1e7b1c392" + integrity sha512-5k547tI4Cy+Lddr/hdjNbBEWBwSl8EBc5aSdKvedav8DReADgWJzcYiktaRIw3GtGC1jjwldXtTzvqJZmtvC7w== + +mime-types@2.1.13: + version "2.1.13" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.13.tgz#e07aaa9c6c6b9a7ca3012c69003ad25a39e92a88" + integrity sha512-ryBDp1Z/6X90UvjUK3RksH0IBPM137T7cmg4OgD5wQBojlAiUwuok0QeELkim/72EtcYuNlmbkrcGuxj3Kl0YQ== + dependencies: + mime-db "~1.25.0" + mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" @@ -7662,6 +7658,13 @@ pkg-up@^3.1.0: dependencies: find-up "^3.0.0" +podcast@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/podcast/-/podcast-2.0.1.tgz#72167bab5221ec373e38dac5ffb3d1717f82a190" + integrity sha512-TWXe/zVziwJNksAn7RLkSre+Z6VQgbs/+gC7qQCKdkyw0hv2hdFGOY9rHgKqa4LI+UP+yZBa6Wr+b9a9vrYDYQ== + dependencies: + rss "^1.2.2" + postcss-attribute-case-insensitive@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz#03d761b24afc04c09e757e92ff53716ae8ea2741" @@ -8835,6 +8838,14 @@ rollup@^2.43.1: optionalDependencies: fsevents "~2.3.2" +rss@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/rss/-/rss-1.2.2.tgz#50a1698876138133a74f9a05d2bdc8db8d27a921" + integrity sha512-xUhRTgslHeCBeHAqaWSbOYTydN2f0tAzNXvzh3stjz7QDhQMzdgHf3pfgNIngeytQflrFPfy6axHilTETr6gDg== + dependencies: + mime-types "2.1.13" + xml "1.0.1" + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -8947,11 +8958,6 @@ schema-utils@^4.0.0: ajv-formats "^2.1.1" ajv-keywords "^5.0.0" -seamless-immutable@^7.1.3: - version "7.1.4" - resolved "https://registry.yarnpkg.com/seamless-immutable/-/seamless-immutable-7.1.4.tgz#6e9536def083ddc4dea0207d722e0e80d0f372f8" - integrity sha512-XiUO1QP4ki4E2PHegiGAlu6r82o5A+6tRh7IkGGTVg/h+UoeX4nFBeCGPOhb4CYjvkqsfm/TUtvOMYC1xmV30A== - select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -10374,6 +10380,11 @@ xml-name-validator@^3.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== +xml@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" + integrity sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw== + xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"