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 = ` + + + + <![CDATA[ OpenMHz ]]> + + + 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 + ` + + +<![CDATA[ ${podcast.title} ]]> + + + + +${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"