diff --git a/backend/controllers/events.js b/backend/controllers/events.js
index 27bc2e5..50fde5c 100644
--- a/backend/controllers/events.js
+++ b/backend/controllers/events.js
@@ -1,10 +1,15 @@
var ObjectID = require('mongodb').ObjectID;
+const { spawn, spawnSync } = require('node:child_process');
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");
-var { callModel: Call, callSchema } = require("../models/call");
+var { callModel: Call, frozenCallModel: FrozenCall } = require("../models/call");
+
var frontend_server = process.env['REACT_APP_FRONTEND_SERVER'] != null ? process.env['REACT_APP_FRONTEND_SERVER'] : 'https://openmhz.com';
const os = require('os');
var fs = require('fs');
@@ -61,6 +66,7 @@ exports.getEvents = async function (req, res, next) {
async function downloadFile(folderName, url) {
return new Promise((resolve, reject) => {
+ console.log("Trying to download: " + url)
https.get(url, (response) => {
const filename = new URL(url).pathname.split('/').pop();
const filePath = `${folderName}/${filename}`;
@@ -98,10 +104,8 @@ const createEventZip = async (folder, filename, zipFolder) => {
}
}
-const uploadEventZip = (zipFile, s3File) => {
-
- // If not a pro plan, use S3 storage
+const uploadFile = (zipFile, s3File) => {
var s3Src = fs.createReadStream(zipFile);
return new Promise((resolve, reject) => {
const s3Params = {
@@ -124,13 +128,124 @@ const uploadEventZip = (zipFile, s3File) => {
});
}
+
+const ffmpeg = async (command) => {
+// https://stackoverflow.com/questions/68101810/how-to-execute-ffmpeg-commands-synchronously
+ return new Promise((resolve, reject) => {
+ const ffmpeg = spawn("/usr/bin/ffmpeg", command);
+ //ffmpeg.once('error', reject);
+ ffmpeg.stderr.on("data", (data) => {
+ 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
+ });
+ ffmpeg.on("exit", (code, signal) => {
+ if (signal)
+ code = signal;
+ if (code != 0) {
+ reject(`Returned exit code ${code}`);
+ console.log('Error');
+ }
+ else {
+ resolve(); // Call resolve here
+ console.log('DONE');
+ }
+ });
+ });
+};
+
+const escapeMetadata = (input) => {
+ let result = input.replace(/(\\)/g, '\\\\');
+ result=result.replace(/(\;)/g, '\\;');
+ result=result.replace(/(\#)/g, '\\#');
+ result=result.replace(/(\=)/g, '\\=');
+ return result;
+}
+const createChapters = (event) => {
+ let chapters = ";FFMETADATA1\n";
+ let totalTime = 0;
+ chapters = chapters + "title=" + escapeMetadata(event.title) + "\n";
+ chapters = chapters + "artist=OpenMHz\n";
+ for (const call of event.calls) {
+ chapters = chapters + "[CHAPTER]\nTIMEBASE=1/1000\nSTART=" + (totalTime * 1000) + "\n";
+ totalTime = totalTime + call.len;
+ chapters = chapters + "END=" + (totalTime * 1000) + "\n";
+ chapters = chapters + "title=" + escapeMetadata(call.talkgroupDescription) + "\n";
+ }
+ console.log("Chapters: \n" + chapters);
+ return chapters;
+}
+
+const createPodcast = async (tmpEventFolder, podcastFile, event) => {
+ let inputs = [];
+ let command = [];
+ let concat = "";
+ let tmpFile = tmpEventFolder + "/tmp.m4a"
+ for (const call of event.calls) {
+ const filename = tmpEventFolder + "/" + new URL(call.url).pathname.split('/').pop();
+ console.log("Adding to podcast: " + filename)
+ inputs.push(filename);
+
+ }
+ inputs.forEach((value, i) => {
+ concat = concat + "file '" + value + "'\n";
+ });
+ const concatFile = tmpEventFolder + "/FILES.txt"
+ fs.writeFileSync(concatFile, concat, "utf8");
+ // https://video.stackexchange.com/questions/21315/concatenating-split-media-files-using-concat-protocol
+ //ffmpeg -f concat -safe 0 -i mylist.txt -c copy output
+ command.push("-f");
+ command.push("concat");
+ command.push("-safe");
+ command.push("0");
+ command.push("-i");
+ command.push(concatFile);
+ command.push("-acodec");
+ command.push("copy");
+ command.push(tmpFile);
+ console.log("Running command: " + command);
+ await ffmpeg(command);
+
+
+ const chapters = createChapters(event);
+ const chaptersFile = tmpEventFolder + "/CHAPTERS.txt"
+ fs.writeFileSync(chaptersFile, chapters, "utf8");
+ command = [];
+ command.push("-i");
+ command.push(tmpFile)
+ command.push("-i");
+ command.push(chaptersFile)
+ command.push("-map_metadata")
+ command.push("1")
+ command.push("-codec");
+ command.push("copy");
+ command.push(podcastFile)
+ console.log("Running command: " + command);
+ await ffmpeg(command);
+}
+
const cleanupEvent = (folder, filename) => {
fs.rmSync(folder, { recursive: true, force: true });
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.eventUrl = frontend_server + "/events/" + event._id;
+ podcast.save();
+}
+
const packageEvent = (eventId) => {
- const event = "tst"
const tmpdir = os.tmpdir();
return new Promise(async (resolve, reject) => {
var objectId = eventId;
@@ -143,12 +258,15 @@ const packageEvent = (eventId) => {
eventFolder = eventFolder + "_" + shortName;
});
const tmpEventFolder = tmpdir + "/" + eventFolder
- const zipFile = tmpdir + "/" + eventFolder + ".zip"
- const s3File = "events/" + eventFolder + ".zip"
+ const zipFile = tmpdir + "/" + eventFolder + ".zip";
+ const podcastFile = tmpEventFolder + "/" + eventFolder + ".m4a";
+ const s3ZipFile = "events/" + eventFolder + ".zip";
+ const s3PodcastFile = "podcasts/" + eventFolder + ".m4a";
console.log("Using tmp " + tmpEventFolder);
try {
if (!fs.existsSync(tmpEventFolder)) {
+ console.log("Creating tmp Folder");
fs.mkdirSync(tmpEventFolder);
}
@@ -159,10 +277,18 @@ const packageEvent = (eventId) => {
exportEventJson(tmpEventFolder, event);
createEventZip(tmpEventFolder, zipFile, eventFolder);
- await uploadEventZip(zipFile, s3File);
- var url = 'https://' + s3_endpoint + "/" + s3_bucket + "/" + s3File;
- event.downloadUrl = url;
+ await uploadFile(zipFile, s3ZipFile);
+
+ const downloadUrl = 'https://' + s3_endpoint + "/" + s3_bucket + "/" + s3ZipFile;
+ const podcastUrl = 'https://' + s3_endpoint + "/" + s3_bucket + "/" + s3PodcastFile;
+ event.downloadUrl = downloadUrl;
+ event.podcastUrl = podcastUrl;
await event.save();
+ await createPodcast(tmpEventFolder, podcastFile, event);
+ await uploadFile(podcastFile,s3PodcastFile);
+ await savePodcast(event,podcastUrl);
+ console.log("uploaded to: " + podcastUrl);
+
} catch (e) {
console.error("Failed processing event " + e);
@@ -174,7 +300,6 @@ const packageEvent = (eventId) => {
});
};
-
const compareCalls = (a, b) => {
const aTimestamp = new Date(a.time).getTime()
const bTimestamp = new Date(b.time).getTime()
@@ -184,6 +309,51 @@ const compareCalls = (a, b) => {
return 1
}
}
+
+const freezeCalls = async (calls) => {
+ let talkgroups = {}
+ let systems = {}
+ let frozenCalls = []
+
+ const getSystem = async (shortName) => {
+ if (!systems.hasOwnProperty(shortName)) {
+ systems[shortName] = await System.findOne({ "shortName": shortName });
+ }
+ return systems[shortName];
+ }
+
+ const getTalkgroup = async (shortName, tgNum) => {
+
+ if (!talkgroups.hasOwnProperty(shortName)) {
+ talkgroups[shortName] = {};
+ }
+ if (!talkgroups[shortName][tgNum]) {
+ talkgroups[shortName][tgNum] = await Talkgroup.findOne({ "shortName": shortName, "num": tgNum });
+ }
+ return talkgroups[shortName][tgNum];
+ }
+
+ for (const i in calls) {
+ const call = calls[i];
+ const talkgroup = await getTalkgroup(call.shortName, call.talkgroupNum);
+ const system = await getSystem(call.shortName);
+
+ // we do this so we can copy the object data to a new Model that has some additional fields.
+ let frozenCall = call.toObject();
+ delete frozenCall._id;
+ if (system) {
+ frozenCall["systemName"] = system.name;
+ frozenCall.systemDescription = system.description;
+ }
+ if (talkgroup) {
+ frozenCall.talkgroupAlpha = talkgroup.alpha;
+ frozenCall.talkgroupDescription = talkgroup.description;
+ }
+ frozenCalls.push(frozenCall);
+ };
+ return frozenCalls;
+}
+
exports.addNewEvent = async function (req, res, next) {
process.nextTick(async function () {
let event = new Event();
@@ -214,19 +384,22 @@ exports.addNewEvent = async function (req, res, next) {
event.startTime = new Date(calls[calls.length - 1].time);
event.expireTime = new Date();
event.expireTime.setDate(event.startTime.getDate() + 29);
+ event.numCalls = calls.length;
calls.forEach((call) => {
if (event.shortNames.indexOf(call.shortName) === -1) {
event.shortNames.push(call.shortName);
}
});
- event.calls = calls;
- event.numCalls = calls.length;
+ event.calls = await freezeCalls(calls);
+
const savedEvent = await event.save();
console.log("Event ID " + savedEvent._id);
- await packageEvent(savedEvent._id);
const url = "/events/" + event._id;
res.contentType('json');
res.send(JSON.stringify({ url: url }));
+
+ await packageEvent(savedEvent._id);
+
}
catch (err) {
console.error(err);
diff --git a/backend/controllers/uploads.js b/backend/controllers/uploads.js
index d240e34..dc4b061 100644
--- a/backend/controllers/uploads.js
+++ b/backend/controllers/uploads.js
@@ -179,7 +179,7 @@ exports.upload = function (req, res, next) {
try {
if (req.body.call_length) {
- call.len = parseInt(req.body.call_length);
+ call.len = parseFloat(req.body.call_length);
} else {
call.len = (stopTime - time) / 1000; //await callLength(req.file.path);
}
diff --git a/backend/models/call.js b/backend/models/call.js
index 23f2d5f..5823ec1 100644
--- a/backend/models/call.js
+++ b/backend/models/call.js
@@ -26,5 +26,19 @@ var callSchema = mongoose.Schema({
});
+var frozenCallSchema = mongoose.Schema();
+frozenCallSchema.add(callSchema).add({
+ systemName: String,
+ systemDescription: String,
+ talkgroupDescription: String,
+ talkgroupAlpha: String,
+
+});
+
+
+exports.frozenCallSchema = frozenCallSchema;
+exports.frozenCallModel = mongoose.model('FrozenCall', frozenCallSchema);
+
+
exports.callSchema = callSchema;
exports.callModel = mongoose.model('Call', callSchema);
diff --git a/backend/models/event.js b/backend/models/event.js
index b358e39..da0169b 100644
--- a/backend/models/event.js
+++ b/backend/models/event.js
@@ -1,6 +1,6 @@
var mongoose = require('mongoose');
-var {callSchema} = require('./call');
-console.log(callSchema)
+var {frozenCallSchema} = require('./call');
+
var eventSchema = mongoose.Schema({
title: String,
@@ -9,8 +9,9 @@ var eventSchema = mongoose.Schema({
startTime: Date,
endTime: Date,
downloadUrl: String,
+ podcastUrl: String,
shortNames: [String],
- calls: [callSchema],
+ calls: [frozenCallSchema],
numCalls: Number
},
{
diff --git a/backend/models/podcast.js b/backend/models/podcast.js
new file mode 100644
index 0000000..b7c515b
--- /dev/null
+++ b/backend/models/podcast.js
@@ -0,0 +1,22 @@
+var mongoose = require('mongoose');
+
+
+
+var podcastSchema = mongoose.Schema({
+ title: String,
+ description: String,
+ expireTime: Date,
+ startTime: Date,
+ endTime: Date,
+ downloadUrl: String,
+ systems: [String],
+ numCalls: Number,
+ eventUrl: String,
+ 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..9f8a60c 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"
-
-
-
-
-
-
// -------------------------------------------
@@ -59,8 +53,8 @@ function getCard(req, res, next) {
function (err, item) {
if (item) {
var time = new Date(item.time);
- var timeString = time.toLocaleTimeString('en-US', {timeZone: "America/New_York"});
- var dateString = time.toLocaleDateString('en-US', {timeZone: "America/New_York"});
+ var timeString = time.toLocaleTimeString('en-US', { timeZone: "America/New_York" });
+ var dateString = time.toLocaleDateString('en-US', { timeZone: "America/New_York" });
//console.log(item)
res.render('card.ejs', {
item: item,
@@ -114,6 +108,173 @@ exports.get_card = function(req, res) {
});
}
*/
+/*
+async function getPodcast(req, res, next) {
+ const now = new Date().toLocaleString();
+
+ const feedStart = `
+
+
+
+
+
+
+ OpenMHz is a website that makes it easy to share recordings of public safety radio system. This podcast is a collection of interesting events that has been curated by the OpenMHz community. ]]>
+
+ https://openmhz.com
+ Podcast for Node
+ ${now}
+
+
+
+
+ ${now}
+
+
+
+
+
+
+ 60
+ OpenMHz
+ I am a sub title
+ Real public safety radio recordings, from across the country. OpenMHz is a website that makes it easy to share recordings of public safety radio system. This podcast is a collection of interesting events that has been curated by the OpenMHz community.
+
+ Luke Berndt
+ luke@robotastic.com
+
+ false
+
+
+
+
+ `
+
+
+
+ const feedEnd = `
+
+
+`
+ let xml = feedStart;
+ try {
+ const podcastsCollection = db.get().collection('podcasts');
+ podcastsCollection.find().toArray(function (err, podcasts) {
+
+
+ for (const podcast of podcasts) {
+ let description = podcast.description + "\n\nRadio Systems: \n";
+ for (const system of podcast.systems) {
+ description = description + " - " + system + "\n";
+ }
+ xml = xml + `
+-
+
+
+
+
+
+
+${podcast.downloadUrl}
+
+
+
+${podcast.eventUrl}
+${podcast.startTime}
+
+OpenMHz
+${description}
+false
+
+`
+ }
+
+
+ xml = xml + feedEnd;
+ res.set('Content-Type', 'text/xml');
+ res.send(xml);
+ });
+
+ } catch (err) {
+ console.error(err);
+ return;
+ }
+}*/
+const dateRange = (start, end) => {
+ if (start.getYear() != end.getYear()) {
+ return { "part1": start.toLocaleDateString('en-us', { weekday: "short", year: "numeric", month: "short", day: "numeric" }) + " " + start.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }), "part2": end.toLocaleDateString('en-us', { weekday: "short", year: "numeric", month: "short", day: "numeric" }) + " " + end.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) };
+ } else if (start.getMonth() != end.getMonth()) {
+ return { "part1": start.toLocaleDateString('en-us', { weekday: "short", month: "short", day: "numeric" }) + " " + start.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }), "part2": end.toLocaleDateString('en-us', { weekday: "short", month: "short", day: "numeric" }) + " " + end.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) };
+
+ } else if (start.getDay() != end.getDay()) {
+ return { "part1": start.toLocaleDateString('en-us', { weekday: "short", month: "short", day: "numeric" }) + " " + start.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }), "part2": end.toLocaleDateString('en-us', { weekday: "short", day: "numeric" }) + " " + end.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) };
+
+ } else {
+ return { "part1": start.toLocaleDateString('en-us', { weekday: "short", month: "short", day: "numeric" }) + " " + start.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }), "part2": end.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) };
+
+ }
+}
+
+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 country. OpenMHz is a website that makes it easy to share recordings of public safety radio system. This podcast is a collection of interesting events that has been 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 country. OpenMHz is a website that makes it easy to share recordings of public safety radio system. This podcast is a collection of interesting events that has been 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) {
+ let dates = dateRange(new Date(podcast.startTime), new Date(podcast.endTime))
+ let description = dates.part1 + " → " + dates.part2 + " \n" + podcast.description + `
+
+Radio Systems:
+`;
+ for (const system of podcast.systems) {
+ description = description + " - " + system + "\n"
+ }
+ feed.addItem({
+ title: podcast.title,
+ description: description,
+ enclosure: {
+ url: podcast.downloadUrl, // link to the item
+ },
+ author: 'OpenMHz', // optional - defaults to feed author property
+ url: podcast.eventUrl,
+ date: podcast.startTime, // any format that js Date can parse.
+ itunesAuthor: 'OpenMHz',
+ itunesExplicit: false,
+ itunesSummary: 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) {
@@ -128,27 +289,27 @@ async function getCalls(req, res, next) {
}
try {
- db.get().collection('calls', function (err, transCollection) {
- transCollection.findOne({
- '_id': o_id
- },
- async function (err, item) {
- if (item) {
- var time = new Date(item.time);
- var timeString = time.toLocaleTimeString('en-US', {timeZone: "America/New_York"});
- var dateString = time.toDateString('en-US', {timeZone: "America/New_York"});
- //console.log(item)
- const tg_coll = db.get().collection('talkgroups');
- const tg = await tg_coll.findOne({ "num": item.talkgroupNum, 'shortName': req.params.shortName.toLowerCase()})
- var title = item.len + " second transmission"
+ db.get().collection('calls', function (err, transCollection) {
+ transCollection.findOne({
+ '_id': o_id
+ },
+ async function (err, item) {
+ if (item) {
+ var time = new Date(item.time);
+ var timeString = time.toLocaleTimeString('en-US', { timeZone: "America/New_York" });
+ var dateString = time.toDateString('en-US', { timeZone: "America/New_York" });
+ //console.log(item)
+ const tg_coll = db.get().collection('talkgroups');
+ const tg = await tg_coll.findOne({ "num": item.talkgroupNum, 'shortName': req.params.shortName.toLowerCase() })
+ var title = item.len + " second transmission"
- if (tg) {
- //console.log(tg)
- title = tg.description;
- }
- const callId = req.query["call-id"];
- const callUrl = "https://s3.us-west-1.wasabisys.com/openmhz-west/media/dcfd-1039-1613917169.m4a"
- const twitterMeta = `
+ if (tg) {
+ //console.log(tg)
+ title = tg.description;
+ }
+ const callId = req.query["call-id"];
+ const callUrl = "https://s3.us-west-1.wasabisys.com/openmhz-west/media/dcfd-1039-1613917169.m4a"
+ const twitterMeta = `
@@ -160,19 +321,19 @@ async function getCalls(req, res, next) {
`
- res.render("index.ejs", { TWITTER_META: twitterMeta })
- } else {
- console.warn("Error - /card/:id Could not find Item " + err);
- res.send(404, 'Sorry, we cannot find that!');
- }
- });
- });
-
- } catch (err) {
- console.error(err);
- next()
- return;
- }
+ res.render("index.ejs", { TWITTER_META: twitterMeta })
+ } else {
+ console.warn("Error - /card/:id Could not find Item " + err);
+ res.send(404, 'Sorry, we cannot find that!');
+ }
+ });
+ });
+
+ } catch (err) {
+ console.error(err);
+ next()
+ return;
+ }
} else {
@@ -186,6 +347,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"