🎉
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_style = tab
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
max_line_length = 120
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.json]
|
||||||
|
max_line_length = unset
|
||||||
|
|
||||||
|
[*.yml]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.{html,js,md}]
|
||||||
|
block_comment_start = /**
|
||||||
|
block_comment = *
|
||||||
|
block_comment_end = */
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
## env
|
||||||
|
.env
|
||||||
|
.env.js
|
||||||
|
|
||||||
|
## editors
|
||||||
|
/.idea
|
||||||
|
/.vscode
|
||||||
|
|
||||||
|
## system files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
## npm
|
||||||
|
/node_modules/
|
||||||
|
/npm-debug.log
|
||||||
|
|
||||||
|
## testing
|
||||||
|
/coverage/
|
||||||
|
|
||||||
|
## temp folders
|
||||||
|
/.tmp/
|
||||||
|
|
||||||
|
# build
|
||||||
|
/_site/
|
||||||
|
/dist/
|
||||||
|
/out-tsc/
|
||||||
|
|
||||||
|
storybook-static
|
||||||
|
custom-elements.json
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 app-cfpodcasts
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img width="200" src="./assets/logo.svg"></img>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
# CommaFeed Podcasts
|
||||||
|
**Experimantal/WIP** PWA podcast app. Idea is to use [CommaFeed](https://github.com/Athou/commafeed) as a backend.
|
||||||
|
|
||||||
|
- [Plan](./changelog/PLAN.md), [Task](./changelog/TASK.md)
|
||||||
|
- [Building scripts](./bs/README.md)
|
||||||
|
- [](https://github.com/open-wc)
|
||||||
|
- [What is Lit? – Lit](https://lit.dev/docs/)
|
||||||
|
- [@lit-labs/router - npm](https://www.npmjs.com/package/@lit-labs/router)
|
||||||
|
- [lit-translate - npm](https://www.npmjs.com/package/lit-translate)
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
height="393.84613"
|
||||||
|
width="393.84613"
|
||||||
|
viewBox="0 0 5.0480766 5.0480766"
|
||||||
|
version="1.1"
|
||||||
|
id="svg3"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs3" />
|
||||||
|
<rect
|
||||||
|
fill="#f88a14"
|
||||||
|
rx="0.53846151"
|
||||||
|
ry="0.53846151"
|
||||||
|
height="5.0480766"
|
||||||
|
width="5.0480766"
|
||||||
|
id="rect1"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
style="stroke-width:0.769231" />
|
||||||
|
<path
|
||||||
|
d="m 1.3450904,0.64548657 c 2.9002,0 2.9002,2.91010003 2.9002,2.91010003"
|
||||||
|
fill="none"
|
||||||
|
stroke="#ffffff"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-width="0.78125"
|
||||||
|
id="path1" />
|
||||||
|
<path
|
||||||
|
d="m 1.3377904,1.9915866 c 1.5705,-0.00908 1.5705,1.5639 1.5705,1.5639"
|
||||||
|
fill="none"
|
||||||
|
stroke="#ffffff"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-width="0.78125"
|
||||||
|
id="path2" />
|
||||||
|
<path
|
||||||
|
d="m 2.0192904,3.5227866 c 0,0.23366 -0.10712,0.47418 -0.24663,0.6537 -0.1814,0.2333 -0.5705,0.5618 -0.6913,0.5653 0.0402,-0.0662 0.263,-0.5654 0.2563,-0.5654 -0.36423004,0 -0.65950004,-0.29265 -0.65950004,-0.65365 0,-0.361 0.29527,-0.65365 0.65950004,-0.65365 0.36423,0 0.68159,0.29265 0.68159,0.65365 z"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="path3"
|
||||||
|
style="fill:#c83737" />
|
||||||
|
<path
|
||||||
|
style="fill:#ffffff;stroke:#666666;stroke-width:0"
|
||||||
|
d="m 1.0262395,3.0457825 v 0.944372 L 1.8837176,3.5592476 Z"
|
||||||
|
id="path4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
+22
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# depends on
|
||||||
|
declare -r readme='bs/README.md'
|
||||||
|
|
||||||
|
isHelp() {
|
||||||
|
for arg in "$@"; do
|
||||||
|
[[ "$arg" == '-h' || "$arg" == '--help' ]] && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
echoReadmeInfo() {
|
||||||
|
local -r script="bs/${0##*/}"
|
||||||
|
local info
|
||||||
|
info="$(grep -A1 "## $script" "$readme" | tail -n1)"
|
||||||
|
cat <<-EOF
|
||||||
|
$info
|
||||||
|
Usage: $script [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help: Show this help
|
||||||
|
EOF
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# bs: Build system based on executables
|
||||||
|
This project uses [jaandrle/bs: The simplest possible build system using executable/bash scripts](https://github.com/jaandrle/bs).
|
||||||
|
|
||||||
|
## Available executables
|
||||||
|
If it makes sense, arguments are passed to internally used commands (e.g. `tsc`), e. g. `--help`.
|
||||||
|
|
||||||
|
### bs/lint
|
||||||
|
This lints the project using `tsc`.
|
||||||
|
|
||||||
|
### bs/test
|
||||||
|
This lints the project and runs tests using `wtr`.
|
||||||
|
|
||||||
|
### bs/start
|
||||||
|
This starts the development server using `web-dev-server`.
|
||||||
|
|
||||||
|
### bs/build
|
||||||
|
This builds the project using `rollup`.
|
||||||
|
|
||||||
|
### bs/analyze
|
||||||
|
This analyze Custom Element manifest using the `@custom-elements-manifest/analyzer`.
|
||||||
|
|
||||||
|
### bs/npm/lint
|
||||||
|
Linted projects’ npm dependencies.
|
||||||
|
|
||||||
|
### bs/npm/update
|
||||||
|
Updates projects’ npm dependencies.
|
||||||
|
|
||||||
|
### bs/npm/install-audit
|
||||||
|
Audits projects’ npm dependencies to be installed.
|
||||||
Executable
+23
@@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eo pipefail # this can be harmful, see https://www.youtube.com/watch?v=4Jo3Ml53kvc
|
||||||
|
. bs/.common || {
|
||||||
|
echo 'Please run this script from the project root directory' >&2;
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
# depends on
|
||||||
|
declare -r app='src/app-cfpodcasts.ts'
|
||||||
|
declare -r cem='node_modules/.bin/cem'
|
||||||
|
|
||||||
|
help(){
|
||||||
|
if ! isHelp "${@}"; then return 0; fi
|
||||||
|
echoReadmeInfo
|
||||||
|
echo
|
||||||
|
$cem --help
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
main(){
|
||||||
|
help "${@}"
|
||||||
|
$cem analyze --litelement --globs "$app" "${@}"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "${@}"
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eo pipefail # this can be harmful, see https://www.youtube.com/watch?v=4Jo3Ml53kvc
|
||||||
|
. bs/.common || {
|
||||||
|
echo 'Please run this script from the project root directory' >&2;
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
# depends on
|
||||||
|
declare -r rollup='node_modules/.bin/rollup'
|
||||||
|
declare -r analyze='bs/analyze'
|
||||||
|
declare -r config='rollup.config.js'
|
||||||
|
declare -r dist='dist/'
|
||||||
|
|
||||||
|
help(){
|
||||||
|
if ! isHelp "${@}"; then return 0; fi
|
||||||
|
echoReadmeInfo
|
||||||
|
echo
|
||||||
|
$rollup --help
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
main(){
|
||||||
|
help "${@}"
|
||||||
|
|
||||||
|
rm -rf "$dist"
|
||||||
|
$rollup -c $config "${@}"
|
||||||
|
$analyze --exclude "$dist"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "${@}"
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eo pipefail # this can be harmful, see https://www.youtube.com/watch?v=4Jo3Ml53kvc
|
||||||
|
. bs/.common || {
|
||||||
|
echo 'Please run this script from the project root directory' >&2;
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
# depends on
|
||||||
|
declare -r tsc='node_modules/.bin/tsc'
|
||||||
|
|
||||||
|
help(){
|
||||||
|
if ! isHelp "${@}"; then return 0; fi
|
||||||
|
echoReadmeInfo
|
||||||
|
echo
|
||||||
|
$tsc --help
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
main(){
|
||||||
|
help "${@}"
|
||||||
|
$tsc "${@}"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "${@}"
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
npx npq install "$1" --dry-run
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
npx lockfile-lint --path package.json
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
npx npm-check-updates --interactive --format group --cooldown 7
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eo pipefail # this can be harmful, see https://www.youtube.com/watch?v=4Jo3Ml53kvc
|
||||||
|
. bs/.common || {
|
||||||
|
echo 'Please run this script from the project root directory' >&2;
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
# "start:build": "web-dev-server --root-dir dist --app-index index.html --open",
|
||||||
|
# "start": "tsc && concurrently -k -r \"tsc --watch --preserveWatchOutput\" \"web-dev-server\""
|
||||||
|
# depends on
|
||||||
|
declare -r server='node_modules/.bin/web-dev-server'
|
||||||
|
declare -r lint='bs/lint'
|
||||||
|
declare -r index='index.html'
|
||||||
|
|
||||||
|
help(){
|
||||||
|
if ! isHelp "${@}"; then return 0; fi
|
||||||
|
echoReadmeInfo
|
||||||
|
cat <<-EOF
|
||||||
|
Options:
|
||||||
|
./src Starts the development server (default)
|
||||||
|
./dist Starts the production server
|
||||||
|
EOF
|
||||||
|
$server --help
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
main(){
|
||||||
|
help "${@}"
|
||||||
|
|
||||||
|
local -r target="${1:-./src}" # ./src or ./dist
|
||||||
|
if [[ "$target" == './dist' ]]; then
|
||||||
|
# console warns because of config file, use npx serve?
|
||||||
|
$server --root-dir "$target" --app-index $index --open
|
||||||
|
else
|
||||||
|
$lint --watch --preserveWatchOutput &
|
||||||
|
$server &
|
||||||
|
wait
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main "${@}"
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eo pipefail # this can be harmful, see https://www.youtube.com/watch?v=4Jo3Ml53kvc
|
||||||
|
. bs/.common || {
|
||||||
|
echo 'Please run this script from the project root directory' >&2;
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
# depends on
|
||||||
|
declare -r lint='bs/lint'
|
||||||
|
declare -r wtr='node_modules/.bin/wtr'
|
||||||
|
|
||||||
|
help(){
|
||||||
|
if ! isHelp "${@}"; then return 0; fi
|
||||||
|
echoReadmeInfo
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--watch, -w Runs in watch mode
|
||||||
|
EOF
|
||||||
|
$wtr --help
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
main(){
|
||||||
|
help "$1"
|
||||||
|
if [[ "$1" != "--watch" ]]; then
|
||||||
|
$lint
|
||||||
|
$wtr "$@" --coverage
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
$lint --watch --preserveWatchOutput &
|
||||||
|
$wtr "${*}" &
|
||||||
|
wait
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
# CommaFeed Podcasts PWA Implementation Plan
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This plan outlines the implementation of a Progressive Web App (PWA) for managing and listening to podcasts using CommaFeed as the backend. The app will be built with Lit, providing a seamless experience for users to manage their podcast subscriptions, create playlists, and listen to episodes offline.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
1. **User Authentication**: Securely store CommaFeed instance details, username, and password in the browser.
|
||||||
|
2. **Category Management**: Allow users to specify a category for storing podcast feeds.
|
||||||
|
3. **Playlist Management**: Use the "starred" category as a "to be listened" playlist.
|
||||||
|
4. **Offline Listening**: Download audio files for offline playback.
|
||||||
|
5. **Android Auto Integration**: Basic Bluetooth integration for media control.
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
### 1. Setup and Configuration
|
||||||
|
- **Lit Project Setup**: Initialize a new Lit project with Lit or Open Web Components.
|
||||||
|
- **PWA Configuration**: Set up the PWA manifest and service worker.
|
||||||
|
- **State Management**: Implement @lit-labs/signals (Lit Labs) for state management.
|
||||||
|
- **Routing**: Set up @lit-labs/router (Lit Labs)
|
||||||
|
- **Service Worker**: Configure service worker to cache API requests and enable offline functionality.
|
||||||
|
|
||||||
|
### 2. User Authentication
|
||||||
|
- **Login Form**: Create a form to collect CommaFeed instance URL, username, and password.
|
||||||
|
- **Secure Storage**: Use the browser's Credential Management API (`navigator.credentials`) to securely store and retrieve credentials. This provides a more secure and user-friendly way to manage credentials compared to localStorage or sessionStorage.
|
||||||
|
- **API Integration**: Implement API calls to authenticate with the CommaFeed instance using Basic Auth. The authentication logic involves constructing an Authorization header using modern `TextEncoder` API with a fallback to `btoa` for broader compatibility. The implementation is as follows:
|
||||||
|
```javascript
|
||||||
|
const credentials = `${username}:${password}`;
|
||||||
|
const Authorization = `Basic ${toBase64(credentials)}`;
|
||||||
|
const headers = {
|
||||||
|
Authorization,
|
||||||
|
Accept: "application/json"
|
||||||
|
};
|
||||||
|
function toBase64(data) {
|
||||||
|
const existsTextEncoder= typeof TextEncoder !== 'undefined';
|
||||||
|
if (!existsTextEncoder)
|
||||||
|
return btoa(data);
|
||||||
|
const existsUint8ArrayToBase64= typeof Uint8Array.prototype.toBase64 === 'function';
|
||||||
|
if (existsUint8ArrayToBase64)
|
||||||
|
return new TextEncoder().encode(data).toBase64();
|
||||||
|
|
||||||
|
const binString= Array.from(new TextEncoder().encode(data))
|
||||||
|
.map(x=> String.fromCharCode(x)).join("");
|
||||||
|
return btoa(binString);
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
### 3. Category Management
|
||||||
|
- **Category Selection**: Allow users to select or create a category for storing podcast feeds.
|
||||||
|
- **API Integration**: Use CommaFeed API to fetch and manage categories.
|
||||||
|
|
||||||
|
### 4. Podcast Management
|
||||||
|
- **Feed Subscription**: Implement functionality to subscribe to podcast feeds.
|
||||||
|
- **Episode Listing**: Fetch and display episodes from subscribed feeds.
|
||||||
|
- **Filtering**: Filter episodes based on enclosure URL and type to identify podcasts.
|
||||||
|
|
||||||
|
#### Custom OPML import/export
|
||||||
|
CommaFeed API supports OPML import and export, but not category scoped. So we need to re-implement by ourself,
|
||||||
|
meaning reading/writing OPML files and using (un)subscribe API calls to manage subscriptions.
|
||||||
|
|
||||||
|
### 5. Playlist Management
|
||||||
|
- **Starred Category**: Use the "starred" category as a "to be listened" playlist.
|
||||||
|
- **Playlist Creation**: Allow users to create custom playlists using tags with a special naming convention (`playlist-name`).
|
||||||
|
- **Episode Management**: Add and remove episodes from playlists by managing tags.
|
||||||
|
- **Playback Tracking**: Use a special tag convention (`playing-timestamp`) to track the currently playing episode and its timestamp.
|
||||||
|
|
||||||
|
### 6. Offline Listening
|
||||||
|
- **Audio Download**: Implement functionality to download audio files for offline playback.
|
||||||
|
- **Auto download**: Automatically download audio files for when the use setup for wich playlist or “listen later” (`starred`) user setup in the config (page)
|
||||||
|
- **PWA Caching**: Use service worker to cache API requests and audio files for offline access.
|
||||||
|
|
||||||
|
### 7. Android Auto Integration
|
||||||
|
- **Media Session API**: Implement Media Session API for basic Bluetooth integration.
|
||||||
|
- **Media Control**: Ensure play/pause and skip functionality works through Bluetooth.
|
||||||
|
|
||||||
|
### 8. Docker Compose Setup
|
||||||
|
- **Configuration**: Research CommaFeed's official Docker setup for user provisioning and configure `docker-compose.yml` to set `COMMAFEED_HOST` environment variable.
|
||||||
|
- ideally there should be two options:
|
||||||
|
1. “all in one” with fixed commafeed instance (configured in the docker-compose.yml)
|
||||||
|
2. only frontend
|
||||||
|
- **PWA Application**: Set up Nginx to serve the PWA static files.
|
||||||
|
|
||||||
|
## UI considerations
|
||||||
|
- [All elements | Red Hat design system](https://ux.redhat.com/elements/) with web components:
|
||||||
|
- Audio player
|
||||||
|
- Alert
|
||||||
|
- Button, Call to action (link)
|
||||||
|
- Chip (Filter information or indicate that a selection was made)
|
||||||
|
- Dialog
|
||||||
|
- Scheme toggle
|
||||||
|
- Skip link
|
||||||
|
- Spinner
|
||||||
|
- Switch
|
||||||
|
- Tabs
|
||||||
|
- Tag
|
||||||
|
- Timestamp
|
||||||
|
- Video embed
|
||||||
|
- [Components | Web Awesome](https://webawesome.com/docs/components): I can use PRO web components
|
||||||
|
- Button, Button Group
|
||||||
|
- Dropdown
|
||||||
|
- Badge
|
||||||
|
- Callout
|
||||||
|
- Spinner
|
||||||
|
- Tag
|
||||||
|
- Toast
|
||||||
|
- inputs: Radio Group
|
||||||
|
- Icon
|
||||||
|
- Tab, Tab Group
|
||||||
|
- Format Bytes
|
||||||
|
- Format Date
|
||||||
|
- Format Number
|
||||||
|
- Include (Includes give you the power to embed external HTML files into the page.)
|
||||||
|
- Relative Time
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
See [./docs/CommaFeed API.md](./docs/CommaFeed API.md).
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
- **Basic Auth**: Use Basic Auth for authentication. The Authorization header is constructed using modern `TextEncoder` API with a fallback to `btoa` for broader compatibility.
|
||||||
|
|
||||||
|
### Categories
|
||||||
|
- `GET /rest/category/get`: Fetch all categories listed under the `children` key.
|
||||||
|
- `POST /rest/category/add`: Add a new category.
|
||||||
|
- `POST /rest/category/modify`: Modify an existing category.
|
||||||
|
- `POST /rest/category/delete`: Delete a category.
|
||||||
|
|
||||||
|
### Feeds
|
||||||
|
- `GET /rest/feed/get/{id}`: Get feed details.
|
||||||
|
- `POST /rest/feed/subscribe`: Subscribe to a feed.
|
||||||
|
- `POST /rest/feed/unsubscribe`: Unsubscribe from a feed.
|
||||||
|
- `GET /rest/feed/entries`: Get feed entries aka episodes for podcast app.
|
||||||
|
|
||||||
|
### Entries
|
||||||
|
- `GET /rest/entry/tags`: Get list of tags for the user.
|
||||||
|
- `POST /rest/entry/star`: Mark an entry as (un)starred.
|
||||||
|
- `POST /rest/entry/mark`: Mark an entry as read/unread.
|
||||||
|
|
||||||
|
### Playlist Management
|
||||||
|
- `GET /rest/category/entries?id=starred`: Get starred entries.
|
||||||
|
- `POST /rest/entry/star`: Star or unstar an entry.
|
||||||
|
- `GET /rest/entry/tags`: Get list of tags for the user.
|
||||||
|
- `POST /rest/entry/tag`: Add or remove tags from an entry.
|
||||||
|
- Use tags for playlist management (`playlist-name`).
|
||||||
|
- **Playback Tracking**: Use tags to track the currently playing episode and its timestamp (`playing-timestamp`).
|
||||||
|
|
||||||
|
## Data Storage
|
||||||
|
|
||||||
|
### Browser Storage
|
||||||
|
- **Credential Storage**: Use encryption to securely store credentials in the browser.
|
||||||
|
- **PWA** offline practices (cache)
|
||||||
|
- store audio(/video?) files
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
- **Credential Storage**: Use encryption to securely store credentials in the browser.
|
||||||
|
- **API Security**: Ensure all API calls are authenticated and use HTTPS.
|
||||||
|
- **Data Protection**: Protect user data and ensure compliance with privacy regulations.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
- **Unit Testing**: Write unit tests for components and utilities.
|
||||||
|
- **Integration Testing**: Test API integrations and data flow.
|
||||||
|
- **End-to-End Testing**: Test user flows and PWA functionality.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
- **Docker Compose**: Use Docker Compose to deploy the application.
|
||||||
|
- verion “all in one”: use commafeed visible only to the web interface part, asking for host can be removed, needs to read commafeed docker setup docs (how to set up user automatically, probably via config)
|
||||||
|
- only frontend
|
||||||
|
- **CI/CD Pipeline**: Set up a CI/CD pipeline for automated testing and deployment.
|
||||||
|
|
||||||
|
## Timeline
|
||||||
|
- **Week 1-2**: Setup and Configuration
|
||||||
|
- **Week 3-4**: User Authentication and Category Management
|
||||||
|
- **Week 5-6**: Podcast Management and Playlist Management
|
||||||
|
- **Week 7-8**: Offline Listening and Android Auto Integration
|
||||||
|
- **Week 9-10**: Testing and Deployment
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
- [Getting Started – Lit](https://lit.dev/docs/getting-started/)
|
||||||
|
- [Lit Labs – Lit](https://lit.dev/docs/libraries/labs/)
|
||||||
|
- [**Development: Generator: Open Web Components**](https://open-wc.org/docs/development/generator/)
|
||||||
|
- [CommaFeed API Documentation](https://www.commafeed.com/api-documentation/)
|
||||||
|
- [PWA Guide](https://web.dev/progressive-web-apps/)
|
||||||
|
- [Media Session API](https://developer.mozilla.org/en-US/docs/Web/API/MediaSession)
|
||||||
|
- [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache)
|
||||||
|
- [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
|
||||||
|
- [nudeps/nudeps: The DX of a bundler without the complexity of one. Experimentally soft-launched, try it and give us feedback!](https://github.com/nudeps/nudeps)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Ensure compatibility with different browsers and devices.
|
||||||
|
- Focus on user experience and accessibility.
|
||||||
|
- Regularly update dependencies and ensure security patches are applied.
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
---
|
||||||
|
created: 2026-03-28T14:24:51 (UTC +01:00)
|
||||||
|
tags: []
|
||||||
|
source: https://gemini.google.com/
|
||||||
|
author:
|
||||||
|
---
|
||||||
|
# Google Gemini
|
||||||
|
|
||||||
|
## Excerpt
|
||||||
|
|
||||||
|
> Ahoj, zvažuji, že bych naprogramoval PWA aplikaci pro podcasty, jako backend
|
||||||
|
> by sloužila (self-hodsted) instance commafeed (
|
||||||
|
> <https://www.commafeed.com/api-documentation/>) … hlavní věc, která mi u
|
||||||
|
> konkurečních řešení (kromně youtube music) chybí je vytváření playlistů,
|
||||||
|
> zároveň bych chtěl aby šlo na konkrétním zařízení zvolit automatické stahování
|
||||||
|
> audia pro daný playlist (pro offline poslech), chtěl bych aby aplikace šla
|
||||||
|
> používat i přes android auto … celé řešení by mělo být jednoduše nasazovatelné
|
||||||
|
> přes docker compose
|
||||||
|
|
||||||
|
- - -
|
||||||
|
Ahoj! To zní jako naprosto skvělý projekt. Spojit self-hosted RSS čtečku s
|
||||||
|
podcastovou aplikací dává obrovský smysl – podcasty jsou totiž v jádru jen RSS
|
||||||
|
feedy s audio přílohami (`\<enclosure>`). CommaFeed je na tohle super, protože
|
||||||
|
už za tebe řeší parsování feedů a synchronizaci přečtených/nepřečtených položek.
|
||||||
|
|
||||||
|
Pojďme si rozebrat, jak to postavit, kde jsou největší výzvy (hlavně to Android
|
||||||
|
Auto) a jak by mohl vypadat Docker Compose.
|
||||||
|
|
||||||
|
- - -
|
||||||
|
## 🛠️ Architektura a technologie
|
||||||
|
|
||||||
|
### 1\. Backend: CommaFeed
|
||||||
|
|
||||||
|
CommaFeed API ti poskytne všechno, co potřebuješ:
|
||||||
|
|
||||||
|
- Seznam odebíraných podcastů (feedy).
|
||||||
|
- Seznam epizod (záznamy ve feedu).
|
||||||
|
- Přílohy (URL na `.mp3` nebo `.m4a` soubory).
|
||||||
|
- Označení epizody jako "přehrané" (read/unread status).
|
||||||
|
|
||||||
|
### 2\. Frontend: PWA (React / Vue / Svelte)
|
||||||
|
|
||||||
|
Pro uložení audia pro offline poslech v PWA se používá kombinace dvou věcí v
|
||||||
|
prohlížeči:
|
||||||
|
|
||||||
|
- **IndexedDB:** Pro ukládání metadat (názvy playlistů, pozice v přehrávání,
|
||||||
|
seznam epizod).
|
||||||
|
- **Cache API:** Pro ukládání samotných velkých audio souborů (blobů).
|
||||||
|
|
||||||
|
> 💡 **Tip:** Pro stahování velkých souborů na pozadí (i když aplikaci zavřeš) se
|
||||||
|
> podívej na **Background Fetch API**. Pro správu Service Workera doporučuji
|
||||||
|
> knihovnu **Workbox** od Googlu, ušetří ti spoustu vrásek.
|
||||||
|
|
||||||
|
- - -
|
||||||
|
## 🚗 Ta záludná část: Android Auto
|
||||||
|
|
||||||
|
Tady tě musím jako parťák trochu varovat. **Čistá PWA v prohlížeči neumí nativně
|
||||||
|
vykreslit UI na obrazovce Android Auto.** Android Auto vyžaduje nativní integraci
|
||||||
|
přes Android Media3 knihovny. Máš ale tři možnosti, jak to vyřešit:
|
||||||
|
|
||||||
|
1. **Základní Bluetooth (Nejjednodušší):** V PWA implementuješ standardní `
|
||||||
|
Media Session API`. Auto sice neotevře speciální aplikaci na palubovce, ale
|
||||||
|
přes Bluetooth uvidíš název skladby, obal a bude fungovat tlačítko
|
||||||
|
pauza/přeskočit na volantu.
|
||||||
|
2. **TWA / Capacitor obal (Pokročilé):** Zabalíš PWA do Android balíčku (přes
|
||||||
|
Bubblewrap nebo Capacitor). To ti umožní přidat do aplikace kousek
|
||||||
|
nativního Kotlin/Java kódu, který se zaregistruje do Android Auto jako
|
||||||
|
přehrávač a bude komunikovat s tvou PWA.
|
||||||
|
3. **Využití prohlížeče v autě:** Novější auta s Android Automotive (AAOS) mají
|
||||||
|
Vivaldi nebo Chrome přímo v autě, kde by tvoje PWA běžela samostatně.
|
||||||
|
|
||||||
|
- - -
|
||||||
|
## 🐳 Docker Compose (vše v jednom)
|
||||||
|
|
||||||
|
Aby se to dalo nasadit jedním příkazem, můžeme propojit CommaFeed, jeho
|
||||||
|
databázi a tvou novou PWA aplikaci do jednoho souboru.
|
||||||
|
|
||||||
|
```
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# 1. Databáze pro CommaFeed
|
||||||
|
commafeed-db:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: commafeed
|
||||||
|
POSTGRES_USER: commafeed
|
||||||
|
POSTGRES_PASSWORD: secret_password
|
||||||
|
volumes:
|
||||||
|
- cf-db-data:/var/lib/postgresql/data
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# 2. CommaFeed Backend
|
||||||
|
commafeed:
|
||||||
|
image: athou/commafeed:latest
|
||||||
|
ports:
|
||||||
|
- "8082:8082"
|
||||||
|
environment:
|
||||||
|
- SPRING_DATASOURCE_URL=jdbc:postgresql://commafeed-db:5432/commafeed
|
||||||
|
- SPRING_DATASOURCE_USERNAME=commafeed
|
||||||
|
- SPRING_DATASOURCE_PASSWORD=secret_password
|
||||||
|
depends_on:
|
||||||
|
- commafeed-db
|
||||||
|
volumes:
|
||||||
|
- cf-config:/commafeed/config
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# 3. Tvoje nová PWA aplikace (Nginx servírující statické soubory)
|
||||||
|
pwa-podcast-app:
|
||||||
|
image: nginx:alpine # Nebo tvůj vlastní build image
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
volumes:
|
||||||
|
- ./pwa-dist:/usr/share/nginx/html # Složka s tvou zbuilzenou PWA
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
cf-db-data:
|
||||||
|
cf-config:
|
||||||
|
```
|
||||||
|
- - -
|
||||||
|
## ✨ Co dál?
|
||||||
|
|
||||||
|
Tenhle nápad má obrovský potenciál – chybějící playlisty štvou spoustu lidí a
|
||||||
|
self-hosted komunita takové věci miluje.
|
||||||
|
|
||||||
|
Kterou část bys chtěl rozlousknout jako první? **Můžeme se podívat na to, jak
|
||||||
|
přesně strukturovat ukládání audio souborů do Cache API v Service Workeru, nebo
|
||||||
|
jak namodelovat databázi pro ty playlisty?**
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
|||||||
|
export function constructHeaders(username, password) {
|
||||||
|
const Authorization = constructAuthHeader(username, password);
|
||||||
|
return {
|
||||||
|
Authorization,
|
||||||
|
Accept: "application/json"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function constructAuthHeader(username, password) {
|
||||||
|
const credentials = `${username}:${password}`;
|
||||||
|
return `Basic ${toBase64(credentials)}`;
|
||||||
|
}
|
||||||
|
function toBase64(data) {
|
||||||
|
const existsTextEncoder= typeof TextEncoder !== 'undefined';
|
||||||
|
if (!existsTextEncoder)
|
||||||
|
return btoa(data);
|
||||||
|
const existsUint8ArrayToBase64= typeof Uint8Array.prototype.toBase64 === 'function';
|
||||||
|
if (existsUint8ArrayToBase64)
|
||||||
|
return new TextEncoder().encode(data).toBase64();
|
||||||
|
|
||||||
|
const binString= Array.from(new TextEncoder().encode(data))
|
||||||
|
.map(x=> String.fromCharCode(x)).join("");
|
||||||
|
return btoa(binString);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<opml version="1.0">
|
||||||
|
<head>
|
||||||
|
<dateCreated>Tue, 21 Apr 2026 11:06:35 GMT</dateCreated>
|
||||||
|
<title>jaa-podcasty subscriptions in CommaFeed</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<outline text="Epizody z vypršených odběrů" type="rss" title="Epizody z vypršených odběrů" xmlUrl="https://jaandrle.cz/p/2026-04-07.xml" htmlUrl="https://jaandrle.cz/podcasts/" />
|
||||||
|
<outline text="Playlist: Čestmír & Daniela - YouTube" type="rss" title="Playlist: Čestmír & Daniela - YouTube" xmlUrl="http://rss-bridge.jaandrle.cz/?action=display&bridge=YoutubeBridge&token=Chief*Snowplow1*Drastic&context=By+playlist+Id&p=PLPK5bz7v9zh1BFEHQrNri-iCeugTjq1Jj&duration_min=&duration_max=&format=Atom" htmlUrl="http://rss-bridge.jaandrle.cz/?action=display&bridge=YoutubeBridge&token=Chief*Snowplow1*Drastic&context=By+playlist+Id&p=PLPK5bz7v9zh1BFEHQrNri-iCeugTjq1Jj&duration_min=&duration_max=&format=Atom" />
|
||||||
|
<outline text="5:59" type="rss" title="5:59" xmlUrl="https://feeds.transistor.fm/5-59" htmlUrl="https://www.seznamzpravy.cz/" />
|
||||||
|
<outline text="Pinepods News Podcast" type="rss" title="Pinepods News Podcast" xmlUrl="https://news.pinepods.online/feed.xml" htmlUrl="https://news.pinepods.online" />
|
||||||
|
<outline text="Český rozhlas - Věda" type="rss" title="Český rozhlas - Věda" xmlUrl="https://api.mujrozhlas.cz/rss/topic/8c432621-a9a9-4c0a-8376-ea4ae5707fbb.rss" htmlUrl="https://www.mujrozhlas.cz/topic/view/8c432621-a9a9-4c0a-8376-ea4ae5707fbb" />
|
||||||
|
<outline text="Bilance" type="rss" title="Bilance" xmlUrl="https://feeds.transistor.fm/bilance" htmlUrl="https://www.ceskatelevize.cz/porady/14021364946-bilance/" />
|
||||||
|
<outline text="Vlevo dole" type="rss" title="Vlevo dole" xmlUrl="https://feeds.transistor.fm/vlevo-dole" htmlUrl="https://www.seznamzpravy.cz/sekce/vlevo-dole" />
|
||||||
|
<outline text="Afrika" type="rss" title="Afrika" xmlUrl="https://feeds.transistor.fm/afrika" htmlUrl="http://www.voxpot.cz" />
|
||||||
|
<outline text="Přepište dějiny" type="rss" title="Přepište dějiny" xmlUrl="https://anchor.fm/s/404d8bdc/podcast/rss" htmlUrl="https://www.prepistedejiny.cz" />
|
||||||
|
<outline text="PULS" type="rss" title="PULS" xmlUrl="https://feeds.transistor.fm/puls" htmlUrl="https://puls.voxpot.cz" />
|
||||||
|
<outline text="Sféry" type="rss" title="Sféry" xmlUrl="https://predplatne.denikn.cz/podcasts/podcasts/public?code=sfery&token=6d6be77f-3c74-4e95-873c-7eaf48f936a4" htmlUrl="https://denikn.cz/tag/sfery/" />
|
||||||
|
<outline text="Amerika, bejby" type="rss" title="Amerika, bejby" xmlUrl="https://predplatne.denikn.cz/podcasts/podcasts/public?code=amerikabejby&token=6d6be77f-3c74-4e95-873c-7eaf48f936a4" htmlUrl="https://denikn.cz/tag/amerikabejby/" />
|
||||||
|
<outline text="Evropa v souvislostech" type="rss" title="Evropa v souvislostech" xmlUrl="https://www.spreaker.com/show/6034659/episodes/feed" htmlUrl="https://www.spreaker.com/podcast/evropa-v-souvislostech--6034659" />
|
||||||
|
<outline text="Kecy a politika" type="rss" title="Kecy a politika" xmlUrl="https://anchor.fm/s/99c6e0b4/podcast/rss" htmlUrl="https://kecyapolitika.cz" />
|
||||||
|
<outline text="FrontKec podcast" type="rss" title="FrontKec podcast" xmlUrl="https://anchor.fm/s/108d0600c/podcast/rss" htmlUrl="https://www.frontendisti.cz/frontkec" />
|
||||||
|
<outline text="Žárovky" type="rss" title="Žárovky" xmlUrl="https://api.mujrozhlas.cz/rss/podcast/33bb78ff-f079-3bd7-b717-4d6ed480a5b8.rss" htmlUrl="https://www.mujrozhlas.cz/rapi/view/show/33bb78ff-f079-3bd7-b717-4d6ed480a5b8" />
|
||||||
|
<outline text="Smysl pro tumor" type="rss" title="Smysl pro tumor" xmlUrl="https://feeds.transistor.fm/smysl-pro-tumor" htmlUrl="https://www.ceskatelevize.cz" />
|
||||||
|
<outline text="Podcast o Zeměploše Terryho Pratchetta" type="rss" title="Podcast o Zeměploše Terryho Pratchetta" xmlUrl="https://anchor.fm/s/f0417ddc/podcast/rss" htmlUrl="https://podcasters.spotify.com/pod/show/veve3" />
|
||||||
|
<outline text="Online Plus" type="rss" title="Online Plus" xmlUrl="https://api.mujrozhlas.cz/rss/podcast/1e6ac29e-b39d-3d0d-8db2-ce1c74cf75b0.rss" htmlUrl="https://www.mujrozhlas.cz/rapi/view/show/1e6ac29e-b39d-3d0d-8db2-ce1c74cf75b0" />
|
||||||
|
<outline text="IRL: Online Life is Real Life" type="rss" title="IRL: Online Life is Real Life" xmlUrl="https://feeds.simplecast.com/lP7owBq8" htmlUrl="https://irlpodcast.org/" />
|
||||||
|
<outline text="Kocouři paní Figgové" type="rss" title="Kocouři paní Figgové" xmlUrl="https://kocouri.napotitku.cz/feed/podcast/" htmlUrl="https://kocouri.napotitku.cz/podcasts/kocouri-pani-figgove/" />
|
||||||
|
<outline text="Zákulisí sociologie" type="rss" title="Zákulisí sociologie" xmlUrl="https://www.omnycontent.com/d/playlist/452119ab-9cb3-4d72-9381-b2e600f8d1cd/0eda4054-8f27-43dd-98ce-b38a010b760a/c1913cc9-dbdb-4534-951f-b38a010b7613/podcast.rss" htmlUrl="https://talk.youradio.cz/porady/zakulisi-sociologie" />
|
||||||
|
<outline text="Bitcoin a blondýna" type="rss" title="Bitcoin a blondýna" xmlUrl="https://anchor.fm/s/f180aba0/podcast/rss" htmlUrl="https://www.mesec.cz/bitcoin-a-blondyna" />
|
||||||
|
<outline text="S MĚŠCEM V PLUSU" type="rss" title="S MĚŠCEM V PLUSU" xmlUrl="https://anchor.fm/s/1029f4b30/podcast/rss" htmlUrl="https://www.mesec.cz" />
|
||||||
|
<outline text="Destinace Brusel" type="rss" title="Destinace Brusel" xmlUrl="https://api.mujrozhlas.cz/rss/podcast/306aa0d7-12c9-356c-ad32-fee341bc1257.rss" htmlUrl="https://www.mujrozhlas.cz/rapi/view/show/306aa0d7-12c9-356c-ad32-fee341bc1257" />
|
||||||
|
<outline text="Spotlight" type="rss" title="Spotlight" xmlUrl="https://www.spreaker.com/show/5781875/episodes/feed" htmlUrl="https://www.spreaker.com/podcast/spotlight--5781875" />
|
||||||
|
<outline text="Filtr" type="rss" title="Filtr" xmlUrl="https://feeds.captivate.fm/filtr/" htmlUrl="https://pagenotfound.cz/" />
|
||||||
|
<outline text="Lupa.cz" type="rss" title="Lupa.cz" xmlUrl="https://lupacz.libsyn.com/rss" htmlUrl="http://www.lupa.cz" />
|
||||||
|
<outline text="Podcast Živě" type="rss" title="Podcast Živě" xmlUrl="https://www.omnycontent.com/d/playlist/87baeba4-4dea-4967-b7da-b2a300763cfe/fa74f3c8-6b8b-4b1a-8f14-b33f008c2849/57267aa8-2b4e-448e-8606-b33f008c2891/podcast.rss" htmlUrl="https://www.zive.cz/" />
|
||||||
|
<outline text="Podcasty z Matfyzu" type="rss" title="Podcasty z Matfyzu" xmlUrl="https://feed.podbean.com/matfyz/feed.xml" htmlUrl="https://matfyz.podbean.com" />
|
||||||
|
<outline text="Pop Culture Detective: Audio Files – The Pop Culture Detective Agency" type="rss" title="Pop Culture Detective: Audio Files – The Pop Culture Detective Agency" xmlUrl="https://popculturedetective.agency/series/pop-culture-detective-audio-files/feed" htmlUrl="https://popculturedetective.agency" />
|
||||||
|
<outline text="De Facto FSV UK" type="rss" title="De Facto FSV UK" xmlUrl="https://rss.buzzsprout.com/1784031.rss" htmlUrl="https://www.buzzsprout.com/1784031" />
|
||||||
|
<outline text="starojda" type="rss" title="starojda" xmlUrl="https://anchor.fm/s/f4d920c/podcast/rss" htmlUrl="https://www.youtube.com/user/starojda" />
|
||||||
|
<outline text="Škrty" type="rss" title="Škrty" xmlUrl="https://rss.buzzsprout.com/1975085.rss" htmlUrl="https://skrty.buzzsprout.com" />
|
||||||
|
<outline text="Příběh, který se opravdu stal" type="rss" title="Příběh, který se opravdu stal" xmlUrl="https://media.rss.com/pribehkteryseopravdustal/feed.xml" htmlUrl="https://rss.com/podcasts/pribehkteryseopravdustal" />
|
||||||
|
<outline text="Mlčení zabíjí" type="rss" title="Mlčení zabíjí" xmlUrl="https://anchor.fm/s/3d662618/podcast/rss" htmlUrl="https://podcasters.spotify.com/pod/show/mlceni-zabiji" />
|
||||||
|
<outline text="Studio N" type="rss" title="Studio N" xmlUrl="https://predplatne.denikn.cz/podcasts/podcasts/public?code=studio-n&token=6d6be77f-3c74-4e95-873c-7eaf48f936a4" htmlUrl="https://denikn.cz/tag/studio-n/" />
|
||||||
|
<outline text="Vrtěti psem" type="rss" title="Vrtěti psem" xmlUrl="https://predplatne.denikn.cz/podcasts/podcasts/public?code=vrtetipsem&token=6d6be77f-3c74-4e95-873c-7eaf48f936a4" htmlUrl="https://denikn.cz/tag/vrtetipsem/" />
|
||||||
|
<outline text="Chyba systému" type="rss" title="Chyba systému" xmlUrl="https://api.mujrozhlas.cz/rss/podcast/47833fff-1845-3b97-b263-54fe2c4026b7.rss" htmlUrl="https://www.mujrozhlas.cz/rapi/view/show/47833fff-1845-3b97-b263-54fe2c4026b7" />
|
||||||
|
<outline text="Houpačky" type="rss" title="Houpačky" xmlUrl="https://api.mujrozhlas.cz/rss/podcast/367636b7-b3af-39c0-80be-dd0f5c3b16d8.rss" htmlUrl="https://www.mujrozhlas.cz/rapi/view/show/367636b7-b3af-39c0-80be-dd0f5c3b16d8" />
|
||||||
|
<outline text="Zvídavec Evy Sinkovičové" type="rss" title="Zvídavec Evy Sinkovičové" xmlUrl="https://api.mujrozhlas.cz/rss/podcast/590a0a50-7f99-3cd8-98e4-4240c79f8090.rss" htmlUrl="https://www.mujrozhlas.cz/rapi/view/show/590a0a50-7f99-3cd8-98e4-4240c79f8090" />
|
||||||
|
<outline text="Dataři" type="rss" title="Dataři" xmlUrl="https://api.mujrozhlas.cz/rss/podcast/e20ec382-fadb-35e6-b4c8-a9fcf2c8a1c7.rss" htmlUrl="https://www.mujrozhlas.cz/rapi/view/show/e20ec382-fadb-35e6-b4c8-a9fcf2c8a1c7" />
|
||||||
|
<outline text="Vinohradská 12" type="rss" title="Vinohradská 12" xmlUrl="https://api.mujrozhlas.cz/rss/podcast/ee6095c0-33ac-3526-b8bf-df233af38211.rss" htmlUrl="https://www.mujrozhlas.cz/rapi/view/show/ee6095c0-33ac-3526-b8bf-df233af38211" />
|
||||||
|
<outline text="Romancov & spol." type="rss" title="Romancov & spol." xmlUrl="https://predplatne.denikn.cz/podcasts/podcasts/public?code=romancovaspol&token=6d6be77f-3c74-4e95-873c-7eaf48f936a4" htmlUrl="https://denikn.cz/tag/romancovaspol/" />
|
||||||
|
<outline text="Toulky první republikou" type="rss" title="Toulky první republikou" xmlUrl="https://api.mujrozhlas.cz/rss/podcast/8e6624c4-c26d-3e41-a990-c159e6302a63.rss" htmlUrl="https://www.mujrozhlas.cz/rapi/view/show/8e6624c4-c26d-3e41-a990-c159e6302a63" />
|
||||||
|
<outline text="HACKERKA" type="rss" title="HACKERKA" xmlUrl="https://api.mujrozhlas.cz/rss/podcast/4dbaabce-9cb6-3fe9-afaa-93e88cf8e963.rss" htmlUrl="https://www.mujrozhlas.cz/rapi/view/show/4dbaabce-9cb6-3fe9-afaa-93e88cf8e963" />
|
||||||
|
</body>
|
||||||
|
</opml>
|
||||||
Executable
+32
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { exit } from "node:process";
|
||||||
|
import { log } from "node:console";
|
||||||
|
|
||||||
|
import { users } from "../.env.js";
|
||||||
|
const credentials= users[0];
|
||||||
|
|
||||||
|
const host= "https://rss.jaandrle.cz/";
|
||||||
|
const Authorization= "Basic " + toBase64(credentials);
|
||||||
|
const headers= { Authorization, Accept: "application/json" };
|
||||||
|
|
||||||
|
const rootCategory= await fetch(host+"rest/category/get", { headers }).then(res=> res.json());
|
||||||
|
const unreadEntries= await fetch(host+"rest/category/entries?id=all&readType=unread", { headers }).then(res=> res.json());
|
||||||
|
|
||||||
|
log({
|
||||||
|
rootCategory,
|
||||||
|
unreadEntries,
|
||||||
|
});
|
||||||
|
exit(0);
|
||||||
|
|
||||||
|
function toBase64(data) {
|
||||||
|
const existsTextEncoder= typeof TextEncoder !== 'undefined';
|
||||||
|
if (!existsTextEncoder)
|
||||||
|
return btoa(data);
|
||||||
|
const existsUint8ArrayToBase64= typeof Uint8Array.prototype.toBase64 === 'function';
|
||||||
|
if (existsUint8ArrayToBase64)
|
||||||
|
return new TextEncoder().encode(data).toBase64();
|
||||||
|
|
||||||
|
const binString= Array.from(new TextEncoder().encode(data))
|
||||||
|
.map(x=> String.fromCharCode(x)).join("");
|
||||||
|
return btoa(binString);
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||||
|
<meta name="Description" content="Put your description here.">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="./assets/logo.svg">
|
||||||
|
<base href="/">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
background-color: #ededed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<title>CommaFeed Podcasts</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<app-cfpodcasts></app-cfpodcasts>
|
||||||
|
|
||||||
|
<script type="module" src="./out-tsc/src/app-index.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Generated
+11671
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "app-cfpodcasts",
|
||||||
|
"description": "Webcomponent app-cfpodcasts following open-wc recommendations",
|
||||||
|
"license": "MIT",
|
||||||
|
"author": "Jan Andrle <andrle.jan@centrum.cz>",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "bs/test",
|
||||||
|
"build": "bs/build",
|
||||||
|
"start": "bs/start"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lit": "~3.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@custom-elements-manifest/analyzer": "~0.11",
|
||||||
|
"@open-wc/testing": "~4.0",
|
||||||
|
"@rollup/plugin-babel": "~7.0",
|
||||||
|
"@rollup/plugin-node-resolve": "~16.0",
|
||||||
|
"@types/mocha": "~10.0",
|
||||||
|
"@web/dev-server": "~0.4",
|
||||||
|
"@web/rollup-plugin-html": "~3.1",
|
||||||
|
"@web/rollup-plugin-import-meta-assets": "~2.3",
|
||||||
|
"@web/test-runner": "~0.20",
|
||||||
|
"babel-plugin-template-html-minifier": "~4.1",
|
||||||
|
"deepmerge": "~4.3",
|
||||||
|
"rollup": "~4.59",
|
||||||
|
"rollup-plugin-esbuild": "~6.2",
|
||||||
|
"rollup-plugin-workbox": "~8.1",
|
||||||
|
"tslib": "~2.8",
|
||||||
|
"typescript": "~6.0"
|
||||||
|
},
|
||||||
|
"customElements": "custom-elements.json"
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import nodeResolve from "@rollup/plugin-node-resolve";
|
||||||
|
import babel from "@rollup/plugin-babel";
|
||||||
|
import { rollupPluginHTML as html } from "@web/rollup-plugin-html";
|
||||||
|
import { importMetaAssets } from "@web/rollup-plugin-import-meta-assets";
|
||||||
|
import esbuild from "rollup-plugin-esbuild";
|
||||||
|
import { generateSW } from "rollup-plugin-workbox";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
input: "index.html",
|
||||||
|
output: {
|
||||||
|
entryFileNames: "[hash].js",
|
||||||
|
chunkFileNames: "[hash].js",
|
||||||
|
assetFileNames: "[hash][extname]",
|
||||||
|
format: "es",
|
||||||
|
dir: "dist",
|
||||||
|
},
|
||||||
|
preserveEntrySignatures: false,
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
/** Enable using HTML as rollup entrypoint */
|
||||||
|
html({
|
||||||
|
minify: true,
|
||||||
|
injectServiceWorker: true,
|
||||||
|
serviceWorkerPath: "dist/sw.js",
|
||||||
|
}),
|
||||||
|
/** Resolve bare module imports */
|
||||||
|
nodeResolve(),
|
||||||
|
/** Minify JS, compile JS to a lower language target */
|
||||||
|
esbuild({
|
||||||
|
minify: true,
|
||||||
|
target: ["chrome64", "firefox67", "safari11.1"],
|
||||||
|
}),
|
||||||
|
/** Bundle assets references via import.meta.url */
|
||||||
|
importMetaAssets(),
|
||||||
|
/** Minify html and css tagged template literals */
|
||||||
|
babel({
|
||||||
|
babelHelpers: "bundled",
|
||||||
|
plugins: [
|
||||||
|
[
|
||||||
|
"babel-plugin-template-html-minifier",
|
||||||
|
{
|
||||||
|
modules: { lit: ["html", { name: "css", encapsulation: "style" }] },
|
||||||
|
failOnError: false,
|
||||||
|
strictCSS: true,
|
||||||
|
htmlMinifier: {
|
||||||
|
collapseWhitespace: true,
|
||||||
|
conservativeCollapse: true,
|
||||||
|
removeComments: true,
|
||||||
|
caseSensitive: true,
|
||||||
|
minifyCSS: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
/** Create and inject a service worker */
|
||||||
|
generateSW({
|
||||||
|
globIgnores: ["polyfills/*.js", "nomodule-*.js"],
|
||||||
|
navigateFallback: "/index.html",
|
||||||
|
// where to output the generated sw
|
||||||
|
swDest: join("dist", "sw.js"),
|
||||||
|
// directory to match patterns against to be precached
|
||||||
|
globDirectory: join("dist"),
|
||||||
|
// cache any html js and css by default
|
||||||
|
globPatterns: ["**/*.{html,js,css,webmanifest}"],
|
||||||
|
skipWaiting: true,
|
||||||
|
clientsClaim: true,
|
||||||
|
runtimeCaching: [{ urlPattern: "polyfills/*.js", handler: "CacheFirst" }],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { css } from "lit";
|
||||||
|
export const styles = css`
|
||||||
|
:host {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
font-size: calc(10px + 2vmin);
|
||||||
|
color: #1a2b42;
|
||||||
|
max-width: 960px;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
background-color: var(--app-cfpodcasts-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
margin-top: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-footer {
|
||||||
|
font-size: calc(12px + 0.5vmin);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-footer a {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { LitElement, html } from "lit";
|
||||||
|
import { property, customElement } from "lit/decorators.js";
|
||||||
|
import { styles } from "./app-index.css.js";
|
||||||
|
|
||||||
|
const logo = import.meta.resolve("../../assets/logo.svg");
|
||||||
|
|
||||||
|
@customElement("app-cfpodcasts")
|
||||||
|
export class AppCfpodcasts extends LitElement {
|
||||||
|
@property({ type: String }) header = "My app";
|
||||||
|
static styles = styles;
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<main>
|
||||||
|
<div class="logo"><img alt="open-wc logo" src=${logo} /></div>
|
||||||
|
<h1>${this.header}</h1>
|
||||||
|
|
||||||
|
<p>Edit <code>src/AppCfpodcasts.ts</code> and save to reload.</p>
|
||||||
|
<a
|
||||||
|
class="app-link"
|
||||||
|
href="https://open-wc.org/guides/developing-components/code-examples"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Code examples
|
||||||
|
</a>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<p class="app-footer">
|
||||||
|
🚽 Made with love by
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="https://github.com/open-wc"
|
||||||
|
>open-wc</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { html } from 'lit';
|
||||||
|
import { fixture, expect } from '@open-wc/testing';
|
||||||
|
|
||||||
|
import type { AppCfpodcasts } from '../src/app-index.js';
|
||||||
|
import '../src/app-cfpodcasts.js';
|
||||||
|
|
||||||
|
describe('AppCfpodcasts', () => {
|
||||||
|
let element: AppCfpodcasts;
|
||||||
|
beforeEach(async () => {
|
||||||
|
element = await fixture(html`<app-cfpodcasts></app-cfpodcasts>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a h1', () => {
|
||||||
|
const h1 = element.shadowRoot!.querySelector('h1')!;
|
||||||
|
expect(h1).to.exist;
|
||||||
|
expect(h1.textContent).to.equal('My app');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes the a11y audit', async () => {
|
||||||
|
await expect(element).shadowDom.to.be.accessible();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2021",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"noEmitOnError": true,
|
||||||
|
"lib": ["es2021", "dom", "DOM.Iterable"],
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"importHelpers": true,
|
||||||
|
"outDir": "out-tsc",
|
||||||
|
"sourceMap": true,
|
||||||
|
"inlineSources": true,
|
||||||
|
"rootDir": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// import { hmrPlugin, presets } from "@open-wc/dev-server-hmr";
|
||||||
|
|
||||||
|
/** Use Hot Module replacement by adding --hmr to the start command */
|
||||||
|
const hmr = process.argv.includes("--hmr");
|
||||||
|
|
||||||
|
export default /** @type {import("@web/dev-server").DevServerConfig} */ ({
|
||||||
|
open: "/",
|
||||||
|
watch: !hmr,
|
||||||
|
/** Resolve bare module imports */
|
||||||
|
nodeResolve: {
|
||||||
|
exportConditions: ["browser", "development"],
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Compile JS for older browsers. Requires @web/dev-server-esbuild plugin */
|
||||||
|
// esbuildTarget: "auto"
|
||||||
|
|
||||||
|
/** Set appIndex to enable SPA routing */
|
||||||
|
appIndex: "./index.html",
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
/** Use Hot Module Replacement by uncommenting. Requires @open-wc/dev-server-hmr plugin */
|
||||||
|
// hmr && hmrPlugin({ exclude: ["**/*/node_modules/**/*"], presets: [presets.litElement] }),
|
||||||
|
],
|
||||||
|
|
||||||
|
// See documentation for all available options
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// import { playwrightLauncher } from '@web/test-runner-playwright';
|
||||||
|
|
||||||
|
const filteredLogs = ['Running in dev mode', 'Lit is in dev mode'];
|
||||||
|
|
||||||
|
export default /** @type {import("@web/test-runner").TestRunnerConfig} */ ({
|
||||||
|
/** Test files to run */
|
||||||
|
files: 'out-tsc/test/**/*.test.js',
|
||||||
|
|
||||||
|
/** Resolve bare module imports */
|
||||||
|
nodeResolve: {
|
||||||
|
exportConditions: ['browser', 'development'],
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Filter out lit dev mode logs */
|
||||||
|
filterBrowserLogs(log) {
|
||||||
|
for (const arg of log.args) {
|
||||||
|
if (typeof arg === 'string' && filteredLogs.some(l => arg.includes(l))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Compile JS for older browsers. Requires @web/dev-server-esbuild plugin */
|
||||||
|
// esbuildTarget: 'auto',
|
||||||
|
|
||||||
|
/** Amount of browsers to run concurrently */
|
||||||
|
// concurrentBrowsers: 2,
|
||||||
|
|
||||||
|
/** Amount of test files per browser to test concurrently */
|
||||||
|
// concurrency: 1,
|
||||||
|
|
||||||
|
/** Browsers to run tests on */
|
||||||
|
// browsers: [
|
||||||
|
// playwrightLauncher({ product: 'chromium' }),
|
||||||
|
// playwrightLauncher({ product: 'firefox' }),
|
||||||
|
// playwrightLauncher({ product: 'webkit' }),
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// See documentation for all available options
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user