@@ -7,8 +7,8 @@ android {
|
||||
applicationId "us.materialio.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 207
|
||||
versionName "1.14.3"
|
||||
versionCode 208
|
||||
versionName "1.14.4"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -72,7 +72,11 @@
|
||||
<releases>
|
||||
|
||||
|
||||
<release version="1.14.3" date="2026-2-13">
|
||||
|
||||
<release version="1.14.4" date="2026-2-13">
|
||||
<url>https://github.com/Materialious/Materialious/releases/tag/1.14.4</url>
|
||||
</release>
|
||||
<release version="1.14.3" date="2026-2-13">
|
||||
<url>https://github.com/Materialious/Materialious/releases/tag/1.14.3</url>
|
||||
</release>
|
||||
<release version="1.14.2" date="2026-2-12">
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.14.3",
|
||||
"version": "1.14.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "Materialious",
|
||||
"version": "1.14.3",
|
||||
"version": "1.14.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.14.3",
|
||||
"version": "1.14.4",
|
||||
"description": "Modern material design for YouTube and Invidious.",
|
||||
"author": {
|
||||
"name": "Ward Pearce",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.14.3",
|
||||
"version": "1.14.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm run patch:github && vite dev",
|
||||
|
||||
@@ -252,7 +252,7 @@ export async function getFeed(
|
||||
fetchOptions: RequestInit = {}
|
||||
): Promise<Feed> {
|
||||
if (isYTBackend()) {
|
||||
return getFeedYTjs();
|
||||
return getFeedYTjs(maxResults, page);
|
||||
}
|
||||
|
||||
const path = buildPath('auth/feed');
|
||||
|
||||
@@ -18,10 +18,15 @@ export async function getChannelYTjs(channelId: string): Promise<ChannelPage> {
|
||||
const authorId = innerResults.metadata.url?.split('/')[4] ?? '';
|
||||
|
||||
let authorBanners: Image[] = [];
|
||||
if (innerResults.header?.is(YTNodes.PageHeaderView)) {
|
||||
authorBanners = innerResults.header.banner?.image ?? [];
|
||||
if (
|
||||
innerResults.header?.is(YTNodes.PageHeader) &&
|
||||
innerResults.header.content?.is(YTNodes.PageHeaderView)
|
||||
) {
|
||||
authorBanners = innerResults.header.content.banner?.image ?? [];
|
||||
}
|
||||
|
||||
const description = innerResults.metadata.description ?? '';
|
||||
|
||||
return {
|
||||
type: 'channel',
|
||||
allowedRegions: innerResults.metadata.available_countries ?? [],
|
||||
@@ -38,8 +43,8 @@ export async function getChannelYTjs(channelId: string): Promise<ChannelPage> {
|
||||
authorUrl: `/channel/${authorId}`,
|
||||
subCount: 0,
|
||||
autoGenerated: false,
|
||||
description: '',
|
||||
descriptionHml: ''
|
||||
description,
|
||||
descriptionHml: description
|
||||
};
|
||||
}
|
||||
|
||||
@@ -91,7 +96,6 @@ function fetchChannelContentVideosWithContinuation(
|
||||
}
|
||||
|
||||
async function fetchChannelContentWithContinuation(
|
||||
channelId: string,
|
||||
innerResults: YT.Channel | YT.ChannelListContinuation,
|
||||
author: string
|
||||
): Promise<ChannelContent> {
|
||||
@@ -131,5 +135,5 @@ export async function getChannelContentYTjs(
|
||||
innerResults = await channel.getLiveStreams();
|
||||
}
|
||||
|
||||
return fetchChannelContentWithContinuation(channelId, innerResults, author);
|
||||
return fetchChannelContentWithContinuation(innerResults, author);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,11 @@ import { relativeTimestamp } from '$lib/time';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Feed, Subscription, Thumbnail } from '../model';
|
||||
import { getChannelYTjs } from './channel';
|
||||
import { engineCooldownYTStore, engineCullYTStore } from '$lib/store';
|
||||
import {
|
||||
engineCooldownYTStore,
|
||||
engineCullYTStore,
|
||||
engineMaxConcurrentChannelsStore
|
||||
} from '$lib/store';
|
||||
|
||||
export async function getSubscriptionsYTjs(): Promise<Subscription[]> {
|
||||
const subscriptions: Subscription[] = [];
|
||||
@@ -134,19 +138,28 @@ export async function parseChannelRSS(channelId: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFeedYTjs(): Promise<Feed> {
|
||||
export async function getFeedYTjs(maxResults: number, page: number): Promise<Feed> {
|
||||
const channelSubscriptions = await localDb.channelSubscriptions.toArray();
|
||||
|
||||
const toUpdatePromises: Promise<void>[] = [];
|
||||
|
||||
const now = new Date();
|
||||
|
||||
let totalChannelsToParse = 0;
|
||||
for (const channel of channelSubscriptions) {
|
||||
const lastRSSFetch = new Date(channel.lastRSSFetch);
|
||||
const timeDifference = now.getTime() - lastRSSFetch.getTime();
|
||||
const cooldownTime = get(engineCooldownYTStore) * 60 * 60 * 1000;
|
||||
|
||||
if (timeDifference > cooldownTime) {
|
||||
toUpdatePromises.push(parseChannelRSS(channel.channelId));
|
||||
if (totalChannelsToParse < get(engineMaxConcurrentChannelsStore)) {
|
||||
toUpdatePromises.push(parseChannelRSS(channel.channelId));
|
||||
} else {
|
||||
parseChannelRSS(channel.channelId);
|
||||
}
|
||||
}
|
||||
|
||||
totalChannelsToParse++;
|
||||
}
|
||||
|
||||
if (toUpdatePromises) {
|
||||
@@ -166,8 +179,11 @@ export async function getFeedYTjs(): Promise<Feed> {
|
||||
videos = videos.slice(0, cullAfter);
|
||||
}
|
||||
|
||||
const start = (page - 1) * maxResults;
|
||||
const end = start + maxResults;
|
||||
|
||||
return {
|
||||
notifications: [],
|
||||
videos: videos
|
||||
videos: videos.slice(start, end)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,10 +84,13 @@ export async function getVideoYTjs(videoId: string): Promise<VideoPlay> {
|
||||
|
||||
// Unsupported format fix
|
||||
// https://github.com/LuanRT/googlevideo/issues/42
|
||||
if (video.streaming_data)
|
||||
if (video.streaming_data) {
|
||||
video.streaming_data.adaptive_formats = video.streaming_data.adaptive_formats.filter(
|
||||
(format) => format.xtags !== 'CgcKAnZiEgEx'
|
||||
);
|
||||
} else {
|
||||
throw new Error('Video did not provide streaming data.');
|
||||
}
|
||||
|
||||
const adaptiveFormats: AdaptiveFormats[] = [];
|
||||
video.streaming_data?.adaptive_formats.forEach((format) => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import { createEventDispatcher, onMount, tick } from 'svelte';
|
||||
import { _ } from '$lib/i18n';
|
||||
@@ -151,31 +150,31 @@
|
||||
</div>
|
||||
{#if $interfaceSearchSuggestionsStore}
|
||||
{#each suggestionsForSearch as suggestion, index (index)}
|
||||
<li>
|
||||
<a
|
||||
<li class="no-padding">
|
||||
<button
|
||||
onclick={() => {
|
||||
search = suggestion;
|
||||
onSubmit();
|
||||
}}
|
||||
class="transparent suggestion"
|
||||
class:selected={index === selectedSuggestionIndex}
|
||||
href={resolve(`/search/[search]`, { search: encodeURIComponent(suggestion) })}
|
||||
>
|
||||
<div>{suggestion}</div>
|
||||
</a>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if !suggestionsForSearch.length && $interfaceSearchHistoryEnabled}
|
||||
{#each $searchHistoryStore as history (history)}
|
||||
<li>
|
||||
<a
|
||||
<li class="no-padding">
|
||||
<button
|
||||
onclick={() => {
|
||||
search = history;
|
||||
}}
|
||||
href={resolve(`/search/[search]`, { search: encodeURIComponent(history) })}
|
||||
class="transparent suggestion"
|
||||
>
|
||||
<div>{history}</div>
|
||||
</a>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
@@ -192,6 +191,17 @@
|
||||
.selected {
|
||||
background-color: var(--surface-variant);
|
||||
}
|
||||
|
||||
button.suggestion {
|
||||
width: 100%;
|
||||
box-sizing: content-box;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
li:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1140px) {
|
||||
.search {
|
||||
width: 100%;
|
||||
|
||||
@@ -250,7 +250,9 @@
|
||||
<div class="max">
|
||||
{video.viewCountText ?? cleanNumber(video.viewCount ?? 0)}
|
||||
•
|
||||
{isYTBackend() ? relativeTimestamp(video.published, false) : video.publishedText}
|
||||
{isYTBackend() && video.published !== 0
|
||||
? relativeTimestamp(video.published, false)
|
||||
: video.publishedText}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
engineCooldownYTStore,
|
||||
engineCullYTStore,
|
||||
engineFallbacksStore,
|
||||
engineMaxConcurrentChannelsStore,
|
||||
instanceStore
|
||||
} from '$lib/store';
|
||||
import { useEngineFallback, type EngineFallback } from '$lib/api/misc';
|
||||
@@ -115,10 +116,22 @@
|
||||
engineCooldownYTStore.set(Number((event.target as HTMLInputElement).value));
|
||||
}}
|
||||
value={$engineCooldownYTStore}
|
||||
name="cull"
|
||||
name="cooldown"
|
||||
type="number"
|
||||
/>
|
||||
<label for="cull">{$_('layout.backendEngine.cooldown')}</label>
|
||||
<label for="cooldown">{$_('layout.backendEngine.cooldown')}</label>
|
||||
</div>
|
||||
<div class="field label prefix border">
|
||||
<i>pending</i>
|
||||
<input
|
||||
oninput={(event: Event) => {
|
||||
engineMaxConcurrentChannelsStore.set(Number((event.target as HTMLInputElement).value));
|
||||
}}
|
||||
value={$engineMaxConcurrentChannelsStore}
|
||||
name="concurrent"
|
||||
type="number"
|
||||
/>
|
||||
<label for="concurrent">{$_('layout.backendEngine.concurrent')}</label>
|
||||
</div>
|
||||
|
||||
{#if $authStore && $instanceStore}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
playerYouTubeJsFallback
|
||||
} from '../../store';
|
||||
import { playbackRates } from '$lib/player';
|
||||
import { isYTBackend } from '$lib/misc';
|
||||
|
||||
let defaultLanguage = $state(get(playerDefaultLanguage));
|
||||
|
||||
@@ -121,7 +122,7 @@
|
||||
<i>arrow_drop_down</i>
|
||||
</div>
|
||||
|
||||
{#if Capacitor.isNativePlatform()}
|
||||
{#if Capacitor.isNativePlatform() && !isYTBackend()}
|
||||
<div class="field suffix border label">
|
||||
<select
|
||||
tabindex="0"
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
const tabs: { id: string; label: string; icon: string; component: Component }[] = [
|
||||
{ id: 'interface', label: $_('layout.interface'), icon: 'grid_view', component: Interface },
|
||||
{ id: 'player', label: $_('layout.player.title'), icon: 'smart_display', component: Player },
|
||||
{ id: 'ryd', label: 'RYD', icon: 'thumb_down', component: Ryd },
|
||||
{ id: 'ryd', label: 'Return YT Dislike', icon: 'thumb_down', component: Ryd },
|
||||
{ id: 'api extended', label: 'API Extended', icon: 'sync', component: ApiExtended },
|
||||
{ id: 'sponsorblock', label: 'Sponsorblock', icon: 'block', component: SponsorBlock },
|
||||
{
|
||||
@@ -39,7 +39,7 @@
|
||||
tabs.splice(1, 0, {
|
||||
id: 'engine',
|
||||
label: $_('layout.engine'),
|
||||
icon: 'build',
|
||||
icon: 'rocket_launch',
|
||||
component: Engine
|
||||
});
|
||||
}
|
||||
|
||||
@@ -179,6 +179,7 @@
|
||||
"warning": "Advanced settings! For experienced users only. Do not change unless you know what you’re doing.",
|
||||
"cull": "Max feed items",
|
||||
"cooldown": "Re-fetch channel hour cooldown",
|
||||
"concurrent": "Max amount of channels to parse before feed return",
|
||||
"fallbacks": "Fallbacks",
|
||||
"importExport": "Import/Export subscriptions",
|
||||
"exportToInvidious": "Export to Invidious from Materialious",
|
||||
|
||||
@@ -33,11 +33,18 @@ export function goToSearch(searchValue: string) {
|
||||
|
||||
goto(resolve(`/search/[search]`, { search: encodeURIComponent(searchTrimmed) }));
|
||||
|
||||
if (get(interfaceSearchHistoryEnabled) && !get(searchHistoryStore).includes(searchTrimmed)) {
|
||||
if (get(interfaceSearchHistoryEnabled)) {
|
||||
const pastHistory = get(searchHistoryStore);
|
||||
if (pastHistory.length > 15) {
|
||||
|
||||
const index = pastHistory.indexOf(searchTrimmed);
|
||||
if (index !== -1) {
|
||||
pastHistory.splice(index, 1);
|
||||
}
|
||||
|
||||
if (pastHistory.length >= 15) {
|
||||
pastHistory.pop();
|
||||
}
|
||||
|
||||
searchHistoryStore.set([searchTrimmed, ...pastHistory]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,6 +324,11 @@ export const engineCooldownYTStore: Writable<number> = persist(
|
||||
createStorage(),
|
||||
'engineCooldownYT'
|
||||
);
|
||||
export const engineMaxConcurrentChannelsStore: Writable<number> = persist(
|
||||
writable(100),
|
||||
createStorage(),
|
||||
'engineMaxConcurrentChannels'
|
||||
);
|
||||
|
||||
export const engineFallbacksStore: Writable<EngineFallback[]> = persist(
|
||||
writable([]),
|
||||
|
||||
@@ -36,32 +36,46 @@
|
||||
async function loadMore(event: InfiniteEvent) {
|
||||
if (typeof displayContent === 'undefined') return;
|
||||
|
||||
if (typeof displayContent.continuation === 'undefined') {
|
||||
event.detail.complete();
|
||||
return;
|
||||
let completed = false;
|
||||
let newContent: ChannelContent;
|
||||
if (displayContent.getContinuation) {
|
||||
newContent = await displayContent.getContinuation();
|
||||
displayContent.getContinuation = newContent.getContinuation;
|
||||
|
||||
completed = newContent.getContinuation === undefined;
|
||||
} else {
|
||||
if (typeof displayContent.continuation === 'undefined') {
|
||||
event.detail.complete();
|
||||
return;
|
||||
}
|
||||
|
||||
newContent = await getChannelContent(page.params.slug, {
|
||||
type: tab,
|
||||
continuation: displayContent.continuation,
|
||||
sortBy: sortBy
|
||||
});
|
||||
|
||||
completed = displayContent.continuation === newContent.continuation;
|
||||
displayContent.continuation = newContent.continuation;
|
||||
}
|
||||
|
||||
const newContent = await getChannelContent(page.params.slug, {
|
||||
type: tab,
|
||||
continuation: displayContent.continuation,
|
||||
sortBy: sortBy
|
||||
});
|
||||
if ('videos' in newContent && 'videos' in displayContent) {
|
||||
if (displayContent.continuation === newContent.continuation) {
|
||||
if (completed) {
|
||||
event.detail.complete();
|
||||
} else {
|
||||
event.detail.loaded();
|
||||
}
|
||||
|
||||
displayContent.videos = [...displayContent.videos, ...newContent.videos];
|
||||
} else if ('playlists' in displayContent && 'playlists' in newContent) {
|
||||
if (displayContent.continuation === newContent.continuation) {
|
||||
if (completed) {
|
||||
event.detail.complete();
|
||||
} else {
|
||||
event.detail.loaded();
|
||||
}
|
||||
|
||||
displayContent.playlists = [...displayContent.playlists, ...newContent.playlists];
|
||||
}
|
||||
displayContent.continuation = newContent.continuation;
|
||||
}
|
||||
|
||||
async function changeTab(newTab: 'videos' | 'playlists' | 'streams' | 'shorts') {
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
|
||||
const searchCacheItem = $searchCacheStore[data.searchStoreId];
|
||||
|
||||
if (searchCacheItem?.getContinuation) {
|
||||
if (searchCacheItem.getContinuation) {
|
||||
newSearch = await searchCacheItem.getContinuation();
|
||||
|
||||
if (newSearch.getContinuation) {
|
||||
@@ -87,11 +87,6 @@
|
||||
};
|
||||
|
||||
newSearch = await getSearch(data.slug, searchOptions);
|
||||
|
||||
// Set the continuation method if it exists
|
||||
if (newSearch.getContinuation) {
|
||||
searchCacheItem.getContinuation = newSearch.getContinuation;
|
||||
}
|
||||
}
|
||||
|
||||
if (newSearch.length === 0) {
|
||||
|
||||
@@ -6,16 +6,13 @@
|
||||
import ItemsList from '$lib/components/ItemsList.svelte';
|
||||
import { resolve } from '$app/paths';
|
||||
import { _ } from '$lib/i18n';
|
||||
import { isYTBackend } from '$lib/misc';
|
||||
|
||||
let currentPage = 1;
|
||||
let videos: (VideoBase | Video | PlaylistPageVideo)[] = $state($feedCacheStore.subscription);
|
||||
|
||||
async function loadMore(event: InfiniteEvent) {
|
||||
// Not supported or needed on YT backend
|
||||
if (isYTBackend()) return;
|
||||
|
||||
currentPage++;
|
||||
|
||||
const feed = await getFeed(100, currentPage);
|
||||
if (feed.videos.length === 0) {
|
||||
event.detail.complete();
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -3,7 +3,7 @@ import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
LATEST_VERSION = "1.14.3"
|
||||
LATEST_VERSION = "1.14.4"
|
||||
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