Compare commits

..

1 Commits

Author SHA1 Message Date
45678d7e6a
Fixed detection if youtube video is music or not 2023-06-24 03:39:42 +02:00
16 changed files with 812 additions and 1394 deletions

View File

@ -2,7 +2,6 @@ HASHTAG_FILTER = ichlausche,music,musik,nowplaying,tunetuesday,nowlistening
YOUTUBE_API_KEY = CHANGE_ME YOUTUBE_API_KEY = CHANGE_ME
ODESLI_API_KEY = CHANGE_ME ODESLI_API_KEY = CHANGE_ME
MASTODON_INSTANCE = 'metalhead.club' MASTODON_INSTANCE = 'metalhead.club'
MASTODON_ACCESS_TOKEN = 'YOUR_ACCESS_TOKEN_HERE'
BASE_URL = 'https://moshingmammut.phlaym.net' BASE_URL = 'https://moshingmammut.phlaym.net'
VERBOSE = false VERBOSE = false
IGNORE_USERS = @moshhead@metalhead.club IGNORE_USERS = @moshhead@metalhead.club

View File

@ -1,25 +1,16 @@
module.exports = { module.exports = {
root: true, root: true,
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
extends: ['plugin:svelte/recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
plugins: ['@typescript-eslint'], plugins: ['svelte3', '@typescript-eslint'],
ignorePatterns: ['*.cjs'], ignorePatterns: ['*.cjs'],
overrides: [ overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
],
settings: { settings: {
'svelte3/typescript': () => require('typescript') 'svelte3/typescript': () => require('typescript')
}, },
parserOptions: { parserOptions: {
sourceType: 'module', sourceType: 'module',
ecmaVersion: 2020, ecmaVersion: 2020
extraFileExtensions: ['.svelte']
}, },
env: { env: {
browser: true, browser: true,

View File

@ -88,8 +88,6 @@ Copy `apache2.conf.EXAMPLE` to `/etc/apache2/sites-available/moshingmammut.conf`
Domain. If you do not need or want SSL support, remove the whole `<IfModule mod_ssl.c>` block. Domain. If you do not need or want SSL support, remove the whole `<IfModule mod_ssl.c>` block.
If you do, add the path to your SSLCertificateFile and SSLCertificateKeyFile. If you do, add the path to your SSLCertificateFile and SSLCertificateKeyFile.
Modify DocumentRoot and the two Alias and Directory statements, so that thumbnails and avatars are served directly by apache.
Copy `moshing-mammut.service.EXAMPLE` to `/etc/systemd/system/moshing-mammut.service` Copy `moshing-mammut.service.EXAMPLE` to `/etc/systemd/system/moshing-mammut.service`
and set your `User`, `Group`, `ExecStart` and `WorkingDirectory` accordingly. and set your `User`, `Group`, `ExecStart` and `WorkingDirectory` accordingly.
@ -103,13 +101,6 @@ because the API is the only way to check if a YouTube link leads to music or som
If `ODESLI_API_KEY` is unset, your rate limit to the song.link API will be lower. If `ODESLI_API_KEY` is unset, your rate limit to the song.link API will be lower.
Add `MASTODON_ACCESS_TOKEN` as well, see [Creating our application
](https://docs.joinmastodon.org/client/token/#app) in the Mastodon documentation.
`read:statuses` is the only required scope. An access token will be displayed in your settings. Use that!
There are currently no plans to implement an actual authentication flow.
Run `npm run build` and copy the output folder, usually `build` to `$APP_DIR` on your server. Run `npm run build` and copy the output folder, usually `build` to `$APP_DIR` on your server.
#### On your server again #### On your server again

View File

@ -15,23 +15,6 @@
Include /etc/letsencrypt/options-ssl-apache.conf Include /etc/letsencrypt/options-ssl-apache.conf
DocumentRoot /home/moshing-mammut/app/
ProxyPass /avatars/ !
ProxyPass /thumbnails/ !
Alias /avatars/ /home/moshing-mammut/app/avatars/
Alias /thumbnails/ /home/moshing-mammut/app/thumbnails/
<Directory "/home/moshing-mammut/app/avatars/">
Require all granted
Header set Cache-Control "public,max-age=31536000,immutable"
</Directory>
<Directory "/home/moshing-mammut/app/thumbnails/">
Require all granted
Header set Cache-Control "public,max-age=31536000,immutable"
</Directory>
ProxyPass / http://localhost:3000/ ProxyPass / http://localhost:3000/
ProxyPassReverse / http://localhost:3000/ ProxyPassReverse / http://localhost:3000/

1997
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "moshing-mammut", "name": "moshing-mammut",
"version": "1.3.2", "version": "1.3.1",
"private": true, "private": true,
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"scripts": { "scripts": {
@ -14,9 +14,8 @@
"format": "prettier --plugin-search-dir . --write ." "format": "prettier --plugin-search-dir . --write ."
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-node": "^2.0.0", "@sveltejs/adapter-node": "^1.2.3",
"@sveltejs/kit": "^2.0.0", "@sveltejs/kit": "^1.5.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/node": "^18.16.3", "@types/node": "^18.16.3",
"@types/sqlite3": "^3.1.8", "@types/sqlite3": "^3.1.8",
"@types/ws": "^8.5.4", "@types/ws": "^8.5.4",
@ -25,14 +24,14 @@
"@zerodevx/svelte-toast": "^0.9.3", "@zerodevx/svelte-toast": "^0.9.3",
"eslint": "^8.28.0", "eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte": "^2.35.1", "eslint-plugin-svelte3": "^4.0.0",
"prettier": "^2.8.0", "prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.10.1", "prettier-plugin-svelte": "^2.8.1",
"svelte": "^4.0.0", "svelte": "^3.54.0",
"svelte-check": "^3.4.3", "svelte-check": "^3.0.1",
"tslib": "^2.4.1", "tslib": "^2.4.1",
"typescript": "^5.0.0", "typescript": "^4.9.3",
"vite": "^5.0.0" "vite": "^4.0.0"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {

View File

@ -10,10 +10,6 @@
<meta name="apple-mobile-web-app-title" content="Moshing Mammut" /> <meta name="apple-mobile-web-app-title" content="Moshing Mammut" />
<meta name="application-name" content="Moshing Mammut" /> <meta name="application-name" content="Moshing Mammut" />
<meta name="msapplication-TileColor" content="#2e0b78" /> <meta name="msapplication-TileColor" content="#2e0b78" />
<meta
name="description"
content="A collection of music recommendations and now-listenings by the users of metalhead.club"
/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#17063b" media="(prefers-color-scheme: dark)" /> <meta name="theme-color" content="#17063b" media="(prefers-color-scheme: dark)" />

View File

@ -53,7 +53,7 @@ export const handle = (async ({ event, resolve }) => {
return new Response(f, { headers: [['Content-Type', 'image/' + suffix]] }); return new Response(f, { headers: [['Content-Type', 'image/' + suffix]] });
} catch (e) { } catch (e) {
log.error('no stream', e); log.error('no stream', e);
error(404); throw error(404);
} }
} }

View File

@ -39,7 +39,7 @@
<picture> <picture>
{@html sourceSetHtml} {@html sourceSetHtml}
<img src={account.avatar} alt={avatarDescription} loading="lazy" width="50" height="50" /> <img src={account.avatar} alt={avatarDescription} loading="lazy" />
</picture> </picture>
<style> <style>

View File

@ -18,9 +18,7 @@
$: disabled = !moreAvailable || isLoading; $: disabled = !moreAvailable || isLoading;
$: title = moreAvailable ? 'Load More' : 'There be dragons!'; $: title = moreAvailable ? 'Load More' : 'There be dragons!';
const dispatch = createEventDispatcher<{ const dispatch = createEventDispatcher();
loadOlderPosts: string;
}>();
function loadOlderPosts() { function loadOlderPosts() {
dispatch('loadOlderPosts'); dispatch('loadOlderPosts');

View File

@ -15,39 +15,11 @@
dateCreated = relativeTime($timePassed) ?? absoluteDate; dateCreated = relativeTime($timePassed) ?? absoluteDate;
} }
const songs = filterDuplicates(post.songs ?? []);
function filterDuplicates(songs: SongInfo[]): SongInfo[] {
return songs.filter((obj, index, arr) => {
return arr.map((mapObj) => mapObj.pageUrl).indexOf(obj.pageUrl) === index;
});
}
function getThumbnailSize(song: SongInfo): {
width?: number;
height?: number;
widthSmall?: number;
heightSmall?: number;
} {
if (song.thumbnailWidth === undefined || song.thumbnailHeight === undefined) {
return { width: undefined, height: undefined, widthSmall: undefined, heightSmall: undefined };
}
const factor = 200 / song.thumbnailWidth;
const smallFactor = 60 / song.thumbnailHeight;
const height = song.thumbnailHeight * factor;
return {
width: 200,
height: height,
widthSmall: smallFactor * song.thumbnailWidth,
heightSmall: 60
};
}
// Blurred thumbs aren't generated (yet, unclear of they ever will) // Blurred thumbs aren't generated (yet, unclear of they ever will)
// So blurred forces using the small one, by skipping the others and removing its media query. // So blurred forces using the small one, by skipping the others and removing its media query.
// This is technically unnecessary - the blurred one will only show if it matches the small media query, // This is technically unnecessary - the blurred one will only show if it matches the small media query,
// but this makes it more explicit // but this makes it more explicit
function getSourceSetHtml(song: SongInfo, isBlurred = false): string { function getSourceSetHtml(song: SongInfo, isBlurred: boolean = false): string {
const small = new Map<string, string[]>(); const small = new Map<string, string[]>();
const large = new Map<string, string[]>(); const large = new Map<string, string[]>();
@ -86,16 +58,15 @@
} }
} }
let html = ''; let html = '';
const { width, height, widthSmall, heightSmall } = getThumbnailSize(song);
const mediaAttribute = isBlurred ? '' : 'media="(max-width: 650px)"'; const mediaAttribute = isBlurred ? '' : 'media="(max-width: 650px)"';
for (const entry of small.entries()) { for (const entry of small.entries()) {
const srcset = entry[1].join(', '); const srcset = entry[1].join(', ');
html += `<source srcset="${srcset}" type="${entry[0]}" ${mediaAttribute} width="${widthSmall}" height="${heightSmall}" />`; html += `<source srcset="${srcset}" type="${entry[0]}" ${mediaAttribute} />`;
} }
html += '\n'; html += '\n';
for (const entry of large.entries()) { for (const entry of large.entries()) {
const srcset = entry[1].join(', '); const srcset = entry[1].join(', ');
html += `<source srcset="${srcset}" type="${entry[0]}" width="${width}" height="${height}"/>`; html += `<source srcset="${srcset}" type="${entry[0]}" />`;
} }
return html; return html;
} }
@ -117,7 +88,7 @@
<div class="content">{@html post.content}</div> <div class="content">{@html post.content}</div>
<div class="song"> <div class="song">
{#if post.songs} {#if post.songs}
{#each songs as song (song.pageUrl)} {#each post.songs as song (song.pageUrl)}
<div class="info-wrapper"> <div class="info-wrapper">
<picture> <picture>
{@html getSourceSetHtml(song)} {@html getSourceSetHtml(song)}
@ -132,8 +103,6 @@
class="cover" class="cover"
alt="Cover for {song.artistName} - {song.title}" alt="Cover for {song.artistName} - {song.title}"
loading="lazy" loading="lazy"
width={song.thumbnailWidth}
height={song.thumbnailHeight}
/> />
</picture> </picture>
<span class="text">{song.artistName} - {song.title}</span> <span class="text">{song.artistName} - {song.title}</span>

View File

@ -9,8 +9,6 @@ export type SongInfo = {
thumbnailUrl?: string; thumbnailUrl?: string;
postedUrl: string; postedUrl: string;
resizedThumbnails?: SongThumbnailImage[]; resizedThumbnails?: SongThumbnailImage[];
thumbnailWidth?: number;
thumbnailHeight?: number;
}; };
export type SongwhipReponse = { export type SongwhipReponse = {

View File

@ -40,8 +40,6 @@ type SongRow = {
title?: string; title?: string;
artistName?: string; artistName?: string;
thumbnailUrl?: string; thumbnailUrl?: string;
thumbnailWidth?: number;
thumbnailHeight?: number;
}; };
type AccountAvatarRow = { type AccountAvatarRow = {
@ -93,8 +91,8 @@ if (enableVerboseLog) {
}); });
} }
function applyDbMigration(migration: Migration): Promise<void> { async function applyDbMigration(migration: Migration): Promise<void> {
return new Promise((resolve, reject) => { return new Promise(async (resolve, reject) => {
db.exec(migration.statement, (err) => { db.exec(migration.statement, (err) => {
if (err !== null) { if (err !== null) {
log.error(`Failed to apply migration ${migration.name}`, err); log.error(`Failed to apply migration ${migration.name}`, err);
@ -112,7 +110,7 @@ async function applyMigration(migration: Migration) {
// so filtering won't help // so filtering won't help
const posts = await getPostsInternal(null, null, 10000); const posts = await getPostsInternal(null, null, 10000);
let current = 0; let current = 0;
const total = posts.length.toString().padStart(4, '0'); let total = posts.length.toString().padStart(4, '0');
for (const post of posts) { for (const post of posts) {
current++; current++;
if (post.songs && post.songs.length) { if (post.songs && post.songs.length) {
@ -306,13 +304,6 @@ function getMigrations(): Migration[] {
kind INTEGER NOT NULL, kind INTEGER NOT NULL,
FOREIGN KEY (song_thumbnailUrl) REFERENCES songs(thumbnailUrl) FOREIGN KEY (song_thumbnailUrl) REFERENCES songs(thumbnailUrl)
);` );`
},
{
id: 7,
name: 'song thumbnail size',
statement: `
ALTER TABLE songs ADD COLUMN thumbnailWidth INTEGER NULL;
ALTER TABLE songs ADD COLUMN thumbnailHeight INTEGER NULL;`
} }
]; ];
} }
@ -444,8 +435,8 @@ function saveSongInfoData(postUrl: string, songs: SongInfo[]): Promise<void> {
for (const song of songs) { for (const song of songs) {
db.run( db.run(
` `
INSERT INTO songs (postedUrl, overviewUrl, type, youtubeUrl, title, artistName, thumbnailUrl, post_url, thumbnailWidth, thumbnailHeight) INSERT INTO songs (postedUrl, overviewUrl, type, youtubeUrl, title, artistName, thumbnailUrl, post_url)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, `,
[ [
song.postedUrl, song.postedUrl,
@ -455,9 +446,7 @@ function saveSongInfoData(postUrl: string, songs: SongInfo[]): Promise<void> {
song.title, song.title,
song.artistName, song.artistName,
song.thumbnailUrl, song.thumbnailUrl,
postUrl, postUrl
song.thumbnailWidth,
song.thumbnailHeight
], ],
(songErr) => { (songErr) => {
if (songErr !== null) { if (songErr !== null) {
@ -492,10 +481,7 @@ export async function savePost(post: Post, songs: SongInfo[]) {
await savePostTagData(post); await savePostTagData(post);
log.debug(`Saved ${post.tags.length} tag data ${post.url}`); log.debug(`Saved ${post.tags.length} tag data ${post.url}`);
await saveSongInfoData(post.url, songs); await saveSongInfoData(post.url, songs);
log.debug( log.debug(`Saved ${songs.length} song info data ${post.url}`);
`Saved ${songs.length} song info data ${post.url}`,
songs.map((s) => s.thumbnailHeight)
);
} }
function getPostData(filterQuery: string, params: FilterParameter): Promise<PostRow[]> { function getPostData(filterQuery: string, params: FilterParameter): Promise<PostRow[]> {
@ -548,17 +534,17 @@ function getTagData(postIdsParams: string, postIds: string[]): Promise<Map<strin
}); });
} }
function getSongData(postIdsParams: string, postIds: string[]): Promise<Map<string, SongInfo[]>> { function getSongData(postIdsParams: String, postIds: string[]): Promise<Map<string, SongInfo[]>> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.all( db.all(
`SELECT post_url, songs.postedUrl, songs.overviewUrl, songs.type, songs.youtubeUrl, `SELECT post_url, songs.postedUrl, songs.overviewUrl, songs.type, songs.youtubeUrl,
songs.title, songs.artistName, songs.thumbnailUrl, songs.post_url, songs.thumbnailWidth, songs.thumbnailHeight songs.title, songs.artistName, songs.thumbnailUrl, songs.post_url
FROM songs FROM songs
WHERE post_url IN (${postIdsParams});`, WHERE post_url IN (${postIdsParams});`,
postIds, postIds,
(tagErr, tagRows: SongRow[]) => { (tagErr, tagRows: SongRow[]) => {
if (tagErr != null) { if (tagErr != null) {
log.error('Error loading post songs', tagErr); log.error('Error loading post tags', tagErr);
reject(tagErr); reject(tagErr);
return; return;
} }
@ -571,16 +557,13 @@ function getSongData(postIdsParams: string, postIds: string[]): Promise<Map<stri
title: item.title, title: item.title,
artistName: item.artistName, artistName: item.artistName,
thumbnailUrl: item.thumbnailUrl, thumbnailUrl: item.thumbnailUrl,
postedUrl: item.postedUrl, postedUrl: item.postedUrl
thumbnailHeight: item.thumbnailHeight,
thumbnailWidth: item.thumbnailWidth
} as SongInfo; } as SongInfo;
result.set(item.post_url, [...(result.get(item.post_url) || []), info]); result.set(item.post_url, [...(result.get(item.post_url) || []), info]);
return result; return result;
}, },
new Map() new Map()
); );
log.verbose('songMap', songMap);
resolve(songMap); resolve(songMap);
} }
); );

View File

@ -1,11 +1,10 @@
import { import {
HASHTAG_FILTER, HASHTAG_FILTER,
MASTODON_ACCESS_TOKEN,
MASTODON_INSTANCE, MASTODON_INSTANCE,
ODESLI_API_KEY, ODESLI_API_KEY,
YOUTUBE_API_KEY YOUTUBE_API_KEY
} from '$env/static/private'; } from '$env/static/private';
import { log } from '$lib/log'; import { enableVerboseLog, log } from '$lib/log';
import type { import type {
Account, Account,
AccountAvatar, AccountAvatar,
@ -79,6 +78,24 @@ export class TimelineReader {
const categoryTitle: string = await fetch(youtubeCategoryUrl) const categoryTitle: string = await fetch(youtubeCategoryUrl)
.then((r) => r.json()) .then((r) => r.json())
.then((r) => r.items[0]?.snippet?.title); .then((r) => r.items[0]?.snippet?.title);
if (enableVerboseLog) {
log.verbose(
'Video',
videoId,
'category',
categoryTitle,
'tags',
item.snippet.tags,
'category id',
item.snippet.categoryId,
'response',
respObj,
'snippet',
item.snippet
);
} else {
log.debug('Video', videoId, 'category', categoryTitle);
}
return categoryTitle === 'Music'; return categoryTitle === 'Music';
} }
@ -136,6 +153,7 @@ export class TimelineReader {
const odesliApiUrl = `https://api.song.link/v1-alpha.1/links?${odesliParams}`; const odesliApiUrl = `https://api.song.link/v1-alpha.1/links?${odesliParams}`;
try { try {
const response = await fetch(odesliApiUrl); const response = await fetch(odesliApiUrl);
log.debug('received odesli response', response.status);
if (response.status === 429) { if (response.status === 429) {
throw new Error('Rate limit reached', { cause: 429 }); throw new Error('Rate limit reached', { cause: 429 });
} }
@ -145,8 +163,9 @@ export class TimelineReader {
} }
const info = odesliInfo.entitiesByUniqueId[odesliInfo.entityUniqueId]; const info = odesliInfo.entitiesByUniqueId[odesliInfo.entityUniqueId];
const platform: Platform = 'youtube'; const platform: Platform = 'youtube';
log.debug(url, 'odesli response', info, 'YT URL', odesliInfo.linksByPlatform[platform]?.url);
if (info.platforms.includes(platform)) { if (info.platforms.includes(platform)) {
const youtubeId = let youtubeId =
videoId ?? videoId ??
YOUTUBE_REGEX.exec(url.href)?.groups?.videoId ?? YOUTUBE_REGEX.exec(url.href)?.groups?.videoId ??
new URL(odesliInfo.pageUrl).pathname.split('/y/').pop(); new URL(odesliInfo.pageUrl).pathname.split('/y/').pop();
@ -156,7 +175,7 @@ export class TimelineReader {
} }
const isMusic = await TimelineReader.isMusicVideo(youtubeId); const isMusic = await TimelineReader.isMusicVideo(youtubeId);
if (!isMusic) { if (!isMusic) {
log.debug('Probably not a music video', url); log.debug('Probably not a music video', url, odesliInfo);
return null; return null;
} }
} }
@ -353,11 +372,10 @@ export class TimelineReader {
} }
private startWebsocket() { private startWebsocket() {
const socket = new WebSocket( const socket = new WebSocket(`wss://${MASTODON_INSTANCE}/api/v1/streaming`);
`wss://${MASTODON_INSTANCE}/api/v1/streaming?type=subscribe&stream=public:local&access_token=${MASTODON_ACCESS_TOKEN}`
);
socket.onopen = () => { socket.onopen = () => {
log.log('Connected to WS'); log.log('Connected to WS');
socket.send('{ "type": "subscribe", "stream": "public:local"}');
}; };
socket.onmessage = async (event) => { socket.onmessage = async (event) => {
try { try {
@ -394,8 +412,7 @@ export class TimelineReader {
}; };
socket.onclose = (event) => { socket.onclose = (event) => {
log.warn( log.warn(
`Websocket connection to ${MASTODON_INSTANCE} closed. Code: ${event.code}, reason: '${event.reason}'`, `Websocket connection to ${MASTODON_INSTANCE} closed. Code: ${event.code}, reason: '${event.reason}'`
event
); );
setTimeout(() => { setTimeout(() => {
log.info(`Attempting to reconenct to WS`); log.info(`Attempting to reconenct to WS`);

View File

@ -160,7 +160,7 @@
{#each data.posts as post (post.url)} {#each data.posts as post (post.url)}
<div <div
class="post" class="post"
transition:edgeFly|global={{ transition:edgeFly={{
y: 10, y: 10,
created_at: post.created_at, created_at: post.created_at,
duration: 300, duration: 300,

View File

@ -1,5 +1,5 @@
import adapter from '@sveltejs/adapter-node'; import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import { vitePreprocess } from '@sveltejs/kit/vite';
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
@ -11,14 +11,15 @@ const config = {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter. // If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters. // See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter(), adapter: adapter()
},
csp: { csp: {
directives: { directives: {
'script-src': ['self', 'unsafe-inline'], 'script-src': ['self']
'base-uri': ['self'], },
'object-src': ['none'] reportOnly: {
} 'script-src': ['self']
} }
} }
}; };