Merge pull request #1141 from Materialious/update/1.10.17
Update/1.10.17
This commit is contained in:
@@ -7,8 +7,8 @@ android {
|
||||
applicationId "us.materialio.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 157
|
||||
versionName "1.10.16"
|
||||
versionCode 158
|
||||
versionName "1.10.17"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -67,6 +67,10 @@
|
||||
<content_attribute id="social-contacts">intense</content_attribute>
|
||||
</content_rating>
|
||||
<releases>
|
||||
|
||||
<release version="1.10.17" date="2025-10-16">
|
||||
<url>https://github.com/Materialious/Materialious/releases/tag/1.10.17</url>
|
||||
</release>
|
||||
<release version="1.10.16" date="2025-10-15">
|
||||
<url>https://github.com/Materialious/Materialious/releases/tag/1.10.16</url>
|
||||
</release>
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.10.16",
|
||||
"version": "1.10.17",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "Materialious",
|
||||
"version": "1.10.16",
|
||||
"version": "1.10.17",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.10.16",
|
||||
"version": "1.10.17",
|
||||
"description": "Modern material design for Invidious.",
|
||||
"author": {
|
||||
"name": "Ward Pearce",
|
||||
|
||||
Generated
+16
-29
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.10.15",
|
||||
"version": "1.10.17",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "materialious",
|
||||
"version": "1.10.15",
|
||||
"version": "1.10.17",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.1",
|
||||
@@ -35,7 +35,7 @@
|
||||
"sponsorblock-api": "^0.2.4",
|
||||
"svelte-infinite-loading": "^1.4.0",
|
||||
"svelte-persisted-store": "^0.12.0",
|
||||
"youtubei.js": "^15.1.1"
|
||||
"youtubei.js": "^16.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/assets": "^3.0.5",
|
||||
@@ -7988,18 +7988,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/jintr": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jintr/-/jintr-3.3.1.tgz",
|
||||
"integrity": "sha512-nnOzyhf0SLpbWuZ270Omwbj5LcXUkTcZkVnK8/veJXtSZOiATM5gMZMdmzN75FmTyj+NVgrGaPdH12zIJ24oIA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -8667,6 +8655,15 @@
|
||||
"node": ">=0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/meriyah": {
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/meriyah/-/meriyah-6.1.4.tgz",
|
||||
"integrity": "sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
@@ -12330,15 +12327,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.21.3",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
|
||||
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
@@ -14014,17 +14002,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/youtubei.js": {
|
||||
"version": "15.1.1",
|
||||
"resolved": "https://registry.npmjs.org/youtubei.js/-/youtubei.js-15.1.1.tgz",
|
||||
"integrity": "sha512-fuEDj9Ky6cAQg93BrRVCbr+GTYNZQAIFZrx/a3oDRuGc3Mf5bS0dQfoYwwgjtSV7sgAKQEEdGtzRdBzOc8g72Q==",
|
||||
"version": "16.0.0",
|
||||
"resolved": "https://registry.npmjs.org/youtubei.js/-/youtubei.js-16.0.0.tgz",
|
||||
"integrity": "sha512-aMx+ulnrxzsgVsxTr7gbBVnIjti2NQUlMwCoo1/MzICCJS3iMLOPUFdo7bSpwskL6ljzQ/LxmmB4WQC3FtkBlA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.0.0",
|
||||
"jintr": "^3.3.1",
|
||||
"undici": "^6.21.3"
|
||||
"meriyah": "^6.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/zimmerframe": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.10.16",
|
||||
"version": "1.10.17",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
@@ -68,6 +68,6 @@
|
||||
"sponsorblock-api": "^0.2.4",
|
||||
"svelte-infinite-loading": "^1.4.0",
|
||||
"svelte-persisted-store": "^0.12.0",
|
||||
"youtubei.js": "^15.1.1"
|
||||
"youtubei.js": "^16.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
playerStatisticsByDefault,
|
||||
playerYouTubeJsFallback,
|
||||
playlistSettingsStore,
|
||||
poTokenCacheStore,
|
||||
sponsorBlockCategoriesStore,
|
||||
sponsorBlockDisplayToastStore,
|
||||
sponsorBlockStore,
|
||||
@@ -277,10 +276,24 @@
|
||||
if (isVisible) {
|
||||
player.setTextTrackVisibility(false);
|
||||
} else {
|
||||
const defaultLanguage = get(playerDefaultLanguage);
|
||||
const langCode = ISO6391.getCode(defaultLanguage);
|
||||
let langCode: string;
|
||||
if ($playerDefaultLanguage === 'original') {
|
||||
const languageAndRole = player
|
||||
.getAudioLanguagesAndRoles()
|
||||
.find(({ role }) => role === 'main');
|
||||
|
||||
if (!languageAndRole) {
|
||||
return;
|
||||
}
|
||||
|
||||
langCode = languageAndRole.language;
|
||||
} else {
|
||||
const defaultLanguage = get(playerDefaultLanguage);
|
||||
langCode = ISO6391.getCode(defaultLanguage);
|
||||
}
|
||||
|
||||
const tracks = player.getTextTracks();
|
||||
|
||||
const subtitleTrack = tracks.find((track) => track.language === langCode);
|
||||
|
||||
if (subtitleTrack) {
|
||||
@@ -295,7 +308,7 @@
|
||||
async function loadVideo() {
|
||||
showVideoRetry = false;
|
||||
|
||||
sabrAdapter = injectSabr(data.video, player);
|
||||
sabrAdapter = await injectSabr(data.video, player);
|
||||
|
||||
try {
|
||||
document.getElementsByClassName('shaka-ad-info')[0].remove();
|
||||
@@ -340,8 +353,8 @@
|
||||
}
|
||||
|
||||
if (data.video.captions) {
|
||||
data.video.captions.forEach(async (caption) => {
|
||||
player.addTextTrackAsync(
|
||||
for (const caption of data.video.captions) {
|
||||
await player.addTextTrackAsync(
|
||||
caption.url.startsWith('http') ? caption.url : `${get(instanceStore)}${caption.url}`,
|
||||
caption.language_code,
|
||||
'captions',
|
||||
@@ -349,7 +362,7 @@
|
||||
undefined,
|
||||
caption.label
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (data.content.timestamps) {
|
||||
@@ -495,6 +508,33 @@
|
||||
console.error('Player error:', error);
|
||||
});
|
||||
|
||||
player.getNetworkingEngine()?.registerResponseFilter((type, response, _) => {
|
||||
if (
|
||||
type !== shaka.net.NetworkingEngine.RequestType.SEGMENT ||
|
||||
!response.uri.includes('/api/timedtext')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(response.uri);
|
||||
|
||||
// Fix positioning for auto-generated subtitles
|
||||
// Credit to Freetube!
|
||||
if (
|
||||
url.hostname.endsWith('.youtube.com') &&
|
||||
url.pathname === '/api/timedtext' &&
|
||||
url.searchParams.get('caps') === 'asr' &&
|
||||
url.searchParams.get('kind') === 'asr' &&
|
||||
url.searchParams.get('fmt') === 'vtt'
|
||||
) {
|
||||
const stringBody = new TextDecoder().decode(response.data);
|
||||
// position:0% for LTR text and position:100% for RTL text
|
||||
const cleaned = stringBody.replaceAll(/ align:start position:(?:10)?0%$/gm, '');
|
||||
// @ts-expect-error Type is acceptable, is shaka player
|
||||
response.data = new TextEncoder().encode(cleaned).buffer;
|
||||
}
|
||||
});
|
||||
|
||||
// Required to stop buttons from being still selected when fullscreening
|
||||
document?.addEventListener('fullscreenchange', async () => {
|
||||
const buttons = document.querySelectorAll('.shaka-controls-button-panel button');
|
||||
|
||||
@@ -147,30 +147,32 @@
|
||||
</div>
|
||||
{#if searchSuggestions}
|
||||
{#each suggestionsForSearch as suggestion, index}
|
||||
<a
|
||||
onclick={() => {
|
||||
search = suggestion;
|
||||
handleSubmit();
|
||||
}}
|
||||
class="row"
|
||||
class:selected={index === selectedSuggestionIndex}
|
||||
href={`/search/${encodeURIComponent(suggestion)}`}
|
||||
>
|
||||
<div>{suggestion}</div>
|
||||
</a>
|
||||
<li>
|
||||
<a
|
||||
onclick={() => {
|
||||
search = suggestion;
|
||||
handleSubmit();
|
||||
}}
|
||||
class:selected={index === selectedSuggestionIndex}
|
||||
href={`/search/${encodeURIComponent(suggestion)}`}
|
||||
>
|
||||
<div>{suggestion}</div>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if !suggestionsForSearch.length && $interfaceSearchHistoryEnabled}
|
||||
{#each $searchHistoryStore as history}
|
||||
<a
|
||||
class="row"
|
||||
onclick={() => {
|
||||
search = history;
|
||||
}}
|
||||
href={`/search/${encodeURIComponent(history)}`}
|
||||
>
|
||||
<div>{history}</div>
|
||||
</a>
|
||||
<li>
|
||||
<a
|
||||
onclick={() => {
|
||||
search = history;
|
||||
}}
|
||||
href={`/search/${encodeURIComponent(history)}`}
|
||||
>
|
||||
<div>{history}</div>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
</menu>
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<article class="scroll no-padding" style="height: 75vh;" id="transcript">
|
||||
<article class="scroll border" style="height: 75vh;" id="transcript">
|
||||
<article class="no-elevate" style="position: sticky; top: 0; z-index: 3;">
|
||||
<h6>{$_('transcript')}</h6>
|
||||
<div class="field label suffix border">
|
||||
|
||||
@@ -30,14 +30,14 @@
|
||||
{@html description}
|
||||
</div>
|
||||
|
||||
<article class="border">
|
||||
<nav class="scroll">
|
||||
{#if video.keywords}
|
||||
{#if video.keywords}
|
||||
<article class="border">
|
||||
<nav class="scroll">
|
||||
{#each video.keywords as keyword}
|
||||
<a href={`/search/${encodeURIComponent(keyword)}`} class="chip">{keyword}</a>
|
||||
{/each}
|
||||
{/if}
|
||||
</nav>
|
||||
</article>
|
||||
</nav>
|
||||
</article>
|
||||
{/if}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Inter;
|
||||
src: url('/static/inter.ttf');
|
||||
}
|
||||
|
||||
article {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.shaka-text-container > div {
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.player-theme {
|
||||
font-family: var(--font) !important;
|
||||
color: var(--on-surface) !important;
|
||||
@@ -18,7 +22,7 @@
|
||||
visibility: visible !important;
|
||||
z-index: 99999999 !important;
|
||||
pointer-events: auto !important;
|
||||
max-height: 400px !important;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.shaka-overflow-menu button,
|
||||
@@ -97,3 +101,15 @@
|
||||
visibility: visible !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (width <=400px) {
|
||||
:deep(.shaka-text-container) {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.player-theme .shaka-overflow-menu,
|
||||
.player-theme .shaka-settings-menu {
|
||||
max-height: 80%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { androidPoTokenMinter } from '$lib/android/youtube/minter';
|
||||
import type {
|
||||
AdaptiveFormats,
|
||||
Captions,
|
||||
Image,
|
||||
StoryBoard,
|
||||
Thumbnail,
|
||||
@@ -11,7 +12,27 @@ import { interfaceRegionStore, poTokenCacheStore } from '$lib/store';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import BG, { USER_AGENT } from 'bgutils-js';
|
||||
import { get } from 'svelte/store';
|
||||
import { Innertube, UniversalCache, Utils, YT, YTNodes } from 'youtubei.js';
|
||||
import type { Types } from 'youtubei.js';
|
||||
import { Innertube, UniversalCache, Utils, YT, YTNodes, Platform } from 'youtubei.js';
|
||||
|
||||
Platform.shim.eval = async (
|
||||
data: Types.BuildScriptResult,
|
||||
env: Record<string, Types.VMPrimative>
|
||||
) => {
|
||||
const properties = [];
|
||||
|
||||
if (env.n) {
|
||||
properties.push(`n: exportedVars.nFunction("${env.n}")`);
|
||||
}
|
||||
|
||||
if (env.sig) {
|
||||
properties.push(`sig: exportedVars.sigFunction("${env.sig}")`);
|
||||
}
|
||||
|
||||
const code = `${data.output}\nreturn { ${properties.join(', ')} }`;
|
||||
|
||||
return new Function(code)();
|
||||
};
|
||||
|
||||
export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
|
||||
if (!Capacitor.isNativePlatform()) {
|
||||
@@ -22,8 +43,7 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
|
||||
fetch: fetch,
|
||||
cache: new UniversalCache(false),
|
||||
location: get(interfaceRegionStore),
|
||||
user_agent: USER_AGENT,
|
||||
player_id: '0004de42'
|
||||
user_agent: USER_AGENT
|
||||
});
|
||||
|
||||
const requestKey = 'O43z0dpjhgX20SCx4KAo';
|
||||
@@ -44,7 +64,7 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
|
||||
pyv: true
|
||||
},
|
||||
contentPlaybackContext: {
|
||||
signatureTimestamp: youtube.session.player?.sts
|
||||
signatureTimestamp: youtube.session.player?.signature_timestamp
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -61,13 +81,7 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
|
||||
throw new Error('Unable to pull video info from youtube.js');
|
||||
}
|
||||
|
||||
if (video.basic_info.is_live) {
|
||||
poTokenCacheStore.set(await platformMinter(requestKey, videoId));
|
||||
} else {
|
||||
poTokenCacheStore.set(BG.PoToken.generateColdStartToken(videoId));
|
||||
|
||||
platformMinter(requestKey, videoId).then((poToken) => poTokenCacheStore.set(poToken));
|
||||
}
|
||||
poTokenCacheStore.set(await platformMinter(requestKey, videoId));
|
||||
|
||||
let dashUri: string | undefined;
|
||||
|
||||
@@ -90,7 +104,6 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
|
||||
await video.toDash({
|
||||
manifest_options: {
|
||||
is_sabr: true,
|
||||
captions_format: 'vtt',
|
||||
include_thumbnails: false
|
||||
}
|
||||
})
|
||||
@@ -108,6 +121,27 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
|
||||
authorThumbnails = [];
|
||||
}
|
||||
|
||||
const captions: Captions[] = [];
|
||||
video.captions?.caption_tracks?.forEach((caption) => {
|
||||
const url = new URL(caption.base_url);
|
||||
|
||||
url.searchParams.set('potc', '1');
|
||||
url.searchParams.set('pot', get(poTokenCacheStore) ?? '');
|
||||
url.searchParams.set('c', youtube.session.context.client.clientName);
|
||||
url.searchParams.set('fmt', 'vtt');
|
||||
|
||||
// Remove &xosf=1 as it adds `position:63% line:0%` to the subtitle lines
|
||||
// placing them in the top right corner
|
||||
url.searchParams.delete('xosf');
|
||||
|
||||
captions.push({
|
||||
label: caption.name?.toString() || '',
|
||||
language_code: caption.language_code,
|
||||
// Add correct format to url.
|
||||
url: url.toString()
|
||||
});
|
||||
});
|
||||
|
||||
const recommendedVideos: VideoBase[] = [];
|
||||
video.watch_next_feed?.forEach((recommended: Record<string, any>) => {
|
||||
if (
|
||||
@@ -189,7 +223,7 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
|
||||
formatStreams: [],
|
||||
recommendedVideos: recommendedVideos,
|
||||
authorThumbnails: authorThumbnails,
|
||||
captions: [],
|
||||
captions: captions,
|
||||
authorId: video.basic_info.channel_id || '',
|
||||
authorUrl: `/channel/${video.basic_info.channel_id}`,
|
||||
authorVerified: false,
|
||||
|
||||
@@ -8,7 +8,10 @@ import { get } from 'svelte/store';
|
||||
import { poTokenCacheStore } from '$lib/store';
|
||||
import { buildSabrFormat } from 'googlevideo/utils';
|
||||
|
||||
export function injectSabr(video: VideoPlay, player: shaka.Player): SabrStreamingAdapter | null {
|
||||
export async function injectSabr(
|
||||
video: VideoPlay,
|
||||
player: shaka.Player
|
||||
): Promise<SabrStreamingAdapter | null> {
|
||||
if (!video.ytjs || !Capacitor.isNativePlatform()) return null;
|
||||
|
||||
const sabrAdapter = new SabrStreamingAdapter({
|
||||
@@ -44,7 +47,7 @@ export function injectSabr(video: VideoPlay, player: shaka.Player): SabrStreamin
|
||||
pyv: true
|
||||
},
|
||||
contentPlaybackContext: {
|
||||
signatureTimestamp: video.ytjs.innertube.session.player?.sts
|
||||
signatureTimestamp: video.ytjs.innertube.session.player?.signature_timestamp
|
||||
},
|
||||
reloadPlaybackContext: reloadContext
|
||||
}
|
||||
@@ -56,7 +59,7 @@ export function injectSabr(video: VideoPlay, player: shaka.Player): SabrStreamin
|
||||
video.ytjs.video.cpn
|
||||
);
|
||||
sabrAdapter.setStreamingURL(
|
||||
video.ytjs.innertube.session.player!.decipher(
|
||||
await video.ytjs.innertube.session.player!.decipher(
|
||||
parsedInfo.streaming_data?.server_abr_streaming_url
|
||||
)
|
||||
);
|
||||
@@ -77,7 +80,7 @@ export function injectSabr(video: VideoPlay, player: shaka.Player): SabrStreamin
|
||||
|
||||
if (video.ytjs.video.streaming_data && !isPostLiveDVR && !isLive) {
|
||||
sabrAdapter.setStreamingURL(
|
||||
video.ytjs.innertube.session.player!.decipher(
|
||||
await video.ytjs.innertube.session.player!.decipher(
|
||||
video.ytjs.video.streaming_data?.server_abr_streaming_url
|
||||
)
|
||||
);
|
||||
|
||||
@@ -90,6 +90,8 @@
|
||||
{/if}
|
||||
</article>
|
||||
|
||||
<div class="space"></div>
|
||||
|
||||
{#if data.playlist.videos}
|
||||
<ItemsList
|
||||
items={data.playlist.videos}
|
||||
|
||||
Binary file not shown.
+1
-1
@@ -3,7 +3,7 @@ import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
LATEST_VERSION = "1.10.16"
|
||||
LATEST_VERSION = "1.10.17"
|
||||
RELEASE_DATE = datetime.now().strftime("%Y-%-m-%d") # Format: YYYY-M-D
|
||||
|
||||
WORKING_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "materialious")
|
||||
|
||||
Reference in New Issue
Block a user