For every problem there is always, at least, a solution which seems quite plausible and reasonable. It is simple and clean, direct, neat, and very nice, and yet it is plainly wrong, #Anawim, justtothepoint.com
When I was a little kid, I was really scared of the dark. But then I came to understand, okay, dark just means the absence of photons in the visible wavelength — 400 to 700 nanometers. Then I thought, well, it’s really silly to be afraid of a lack of photons. Then I wasn’t afraid of the dark anymore after that, Elon Musk.

Translating Python list comprehensions into JavaScript chaining. Understand how Python’s [expr for item in iterable if condition] maps to .filter().map() chains and why the order matters.
Parsing a structured plain‑text file into typed objects. Extract key‑value data from a text file using split(), map(), filter(), and destructuring, then wrap the result in a strongly‑typed TypeScript array.
The critical difference between null and undefined in TypeScript. Learn when each value appears, how strict null checks protect your code, and how to design functions that never return null by accident.
Writing a pure service function with no side effects. See how to create a function that only depends on its arguments, returns a new value, and never mutates external state —making it trivially testable.
Displaying a parsed list on a test screen without a database. Render the processed data in a simple UI directly from the in‑memory result, confirming correctness before any backend integration.
A service module src/services/videoService.ts that reads a local file data/prayer_videos.txt, parses each line into a VideoSite object, and returns a clean array. A test screen then consumes that array and renders a preview.
File format (data/prayer_videos.txt):
# Comment line – ignored
Ac_nVJASVfc Peaceful Morning Prayer Music - 1 Hour
eYdmcZUSt0s 10 Minute Morning Prayer with Scripture
Each line contains a YouTube video ID followed by a title, separated by whitespace. Blank lines and lines starting with # are skipped.
Desired output (shown on the Videos tab):
✅ 5 videos loaded
Ac_nVJASVfc
Peaceful Morning Prayer Music - 1 Hour
eYdmcZUSt0s
10 Minute Morning Prayer with Scripture
...
Both Python and JavaScript provide expressive ways to transform lists. In the Python version of the parser, a for loop builds a list by reading all lines, stripping whitespace, skipping unwanted lines, splitting each line, and constructing a dictionary. The JavaScript equivalent can be written as a single pipeline of chainable Array methods. This pattern is not only concise but also encourages immutable data transformations —a perfect fit for a pure service function.
Side‑by‑side comparison
# Python (original logic)
videos = []
for line in VIDEOS_FILE.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split(None, 1) # split on first whitespace, max 2 parts
if parts:
videos.append({
"id": parts[0].strip(),
"title": parts[1].strip() if len(parts) > 1 else parts[0],
})
TypeScript (using array methods):
// TypeScript equivalent using Array methods
const videos = text
.split("\n") // Split into lines, splitlines()
.map(line => line.trim()) // Trim each line, line.strip()
.filter(line => line && !line.startsWith("#")) // Remove blanks & comments
.map(line => {
const [id, ...rest] = line.split(/\s+/); // Split line into part with whitespace, rest captures title
return { // Build object
id,
title: rest.length > 0 ? rest.join(" ") : id,
};
})
.filter(v => v.id.length > 0); // Guard against empty IDs
| Python | JavaScript |
|---|---|
[x for x in items] |
items.map(x => x) |
[x for x in items if condition] |
items.filter(condition).map(x => x) |
[f(x) for x in items if condition] |
items.filter(condition).map(f) |
[f(x) if cond else g(x) for x in items] |
items.map(x => cond ? f(x) : g(x)) |
Note how JavaScript separates filtering and mapping into two methods, whereas Python’s comprehension combines both. In JavaScript, the typical order is .filter(…).map(…), because filter reduces the array before map runs its possibly expensive transformation. This order also mirrors the Python if guard that sits after the iteration variable.
This line does two things at once:
const line = "Ac_nVJASVfc Peaceful Morning Prayer Music";
const [id, ...rest] = line.split(/\s+/);
line.split(/\s+/) splits the string on one or more whitespace characters (spaces, tabs). The regex \s+ ensures that multiple spaces or tabs are treated as a single separator.rest.join(" "), and we get “Peaceful Morning Prayer Music”.# Result:
id = "Ac_nVJASVfc"
rest = ["Peaceful", "Morning", "Prayer", "Music"]
The Python equivalent uses line.split(None, 1) with maxsplit=1, yielding a two‑element list [id, title] (parts[0], parts[1]).
If the title were missing, Python would return [id]. The JavaScript rest array will be empty in that case, and rest.join(" “) gives an empty string —so the ternary rest.length > 0 ? rest.join(" ") : id provides a fallback to the ID itself.
/\s+/ instead of " “?YouTube IDs never contain whitespace, so splitting on any whitespace safely isolates the ID. Using /\s+/ normalises irregular spacing (e.g., accidental double spaces, tabs) without producing empty strings in the array.
It eliminates the need to trim the individual parts afterward.
In the context of this parser, understanding the difference helps you design a robust function.
undefined means a variable has been declared but not assigned a value. It is the default “nothing” in JavaScript. When does it appear? A missing property, an array element that was never set, a function with no explicit return, or destructuring a missing value.
null is an intentional assignment representing “no value”. It is explicitly set by the programmer.
Our service function never returns null because it always returns an array (possibly empty). If the file is empty, we get []. If a line somehow yields an empty ID, the final .filter() removes it. This guarantees the consumer never has to check for null —only for an empty list.
src/services/videoService.ts// src/services/videoService.ts
//
// VIDEO SERVICE
// ──────────────
// Loads and parses a prayer video list from data/prayer_videos.txt.
// Also provides URL builders for embedding, thumbnails, and safe ID extraction.
//
// FILE FORMAT (prayer_videos.txt):
// # comment lines start with #
// VIDEO_ID Title of the video
//
// Example:
// # Morning prayer videos
// Ac_nVJASVfc Peaceful Morning Prayer Music - 1 Hour
// eYdmcZUSt0s 10 Minute Morning Prayer with Scripture
//
// YOUTUBE VIDEO IDs:
// The ID is the value after "v=" in a YouTube URL.
// https://www.youtube.com/watch?v=Ac_nVJASVfc → ID = "Ac_nVJASVfc"
// IDs are always 11 characters: letters, digits, - and _
//
// USAGE:
// import { loadVideos } from "../services/videoService";
// const videos = await loadVideos();
// // videos[0] = { id: "Ac_nVJASVfc", title: "Peaceful Morning Prayer..." }
//
// This module is intentionally simple — no caching, no state management.
// Caching (if needed) belongs in the hook that calls this service.
import { readLines } from "../utils/assetReader";
import type { VideoSite } from "../db/types";
// ══════════════════════════════════════════════════════════════════════════════
// PARSER
// ══════════════════════════════════════════════════════════════════════════════
/**
* Parse a single line of prayer_videos.txt into a VideoSite object.
*
* Expected format: VIDEO_ID Title of the video
* The ID and title are separated by one or more whitespace characters.
*
* @param line A non-empty, non-comment line from the file
* @returns VideoSite object, or null if the line cannot be parsed
*
* @example
* parseLine("Ac_nVJASVfc Peaceful Morning Prayer")
* // → { id: "Ac_nVJASVfc", title: "Peaceful Morning Prayer" }
*
* parseLine(" ") // → null (empty after trim)
* parseLine("# comment") // → null (should have been filtered already)
*/
function parseLine(line: string): VideoSite | null {
// 1. Trim and reject empty / comment lines
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) return null;
// 2. Split on one or more whitespace characters into tokens.
// The first token is assumed to be the video ID. The remaining tokens are joined later to form the title.
//
// Example: "Ac_nVJASVfc Peaceful Morning Prayer Music - 1 Hour"
// → parts = ["Ac_nVJASVfc", "Peaceful", "Morning", "Prayer", "Music", "-", "1", "Hour"]
// → id = "Ac_nVJASVfc"
// → rest = ["Peaceful", "Morning", "Prayer", "Music", "-", "1", "Hour"]
const parts = trimmed.split(/\s+/);
const id = parts[0];
const rest = parts.slice(1); // everything after the first token
// 3. Validate the video ID.
// This checks that the ID: (i) is exactly 11 characters long,
// (ii) contains only letters, digits, hyphens, and underscores.
if (id.length !== 11 || !/^[A-Za-z0-9_-]{11}$/.test(id)) {
// If it fails, the function logs a warning and returns null.
console.warn(`[VideoService] Skipping invalid video ID: "${id}" in line: "${trimmed}"`);
return null;
}
// 4. Build the title.
// If no title tokens are present, fall back to showing the ID itself.
// This avoids returning a VideoSite with an empty title, which could cause UI problems later.
const title = rest.length > 0 ? rest.join(" ").trim() : id;
// 5. Return the object.
return { id, title };
}
// ══════════════════════════════════════════════════════════════════════════════
// LOAD FUNCTION
// ══════════════════════════════════════════════════════════════════════════════
/**
* Load and parse the prayer video list from data/prayer_videos.txt.
* This is the main entry point for fetching the video list.
*
* Reads the bundled text asset, filters out comments and blank lines,
* and converts each valid line into a VideoSite object.
*
* @returns Array of VideoSite objects. Empty array if the file is missing
* or no valid lines are found. NEVER throws — errors are logged.
*
* @example
* const videos = await loadVideos();
* console.log(videos.length); // 5
* console.log(videos[0].id); // "Ac_nVJASVfc"
* console.log(videos[0].title); // "Peaceful Morning Prayer Music - 1 Hour"
*/
export async function loadVideos(): Promise<VideoSite[]> {
try {
// 1. Read the file. readLines internally does:
// 1. Locates the bundled .txt asset via expo-asset.
// 2. Extracts it to a local file URI.
// 3. Reads as UTF-8 text.
// 4. Split into lines, remove comment lines (#) and empty lines.
// 5. Returns a string[].
//
// eslint-disable-next-line @typescript-eslint/no-var-requires
// require("../../data/prayer_videos.txt") is a Metro/Expo‑specific way to get a reference to the bundled text file
// (typically returns an object that readLines understands).
const lines = await readLines(require("../../data/prayer_videos.txt"));
// 2. Parse all lines
// Convert each line to a VideoSite (or null if invalid),
// then remove the nulls.
//
// .map(parseLine) turns each valid line into a VideoSite object or null
// .filter((v): v is VideoSite => v !== null) removes all null entries.
//
// The type predicate v is VideoSite tells TypeScript that after the filter,
// the array only contains VideoSite values. Without that predicate,
// TypeScript would still see the type as (VideoSite | null)[].
const videos: VideoSite[] = lines
.map(parseLine)
.filter((v): v is VideoSite => v !== null);
// 3. Log and return
console.log(`[VideoService] Loaded ${videos.length} video(s) from prayer_videos.txt`);
return videos;
} catch (e) {
// 4. Error handling
// If readLines or anything else throws (e.g., file missing, permission error),
// the error is logged and an empty array is returned. The service never throws, as advertised.
console.error("[VideoService] Failed to load prayer_videos.txt:", e);
return [];
}
}
// ══════════════════════════════════════════════════════════════════════════════
// UTILITIES
// ══════════════════════════════════════════════════════════════════════════════
/**
* Build the YouTube embed URL for a video ID.
*
* Creates a URL that can be used as the source of a <WebView> to embed a YouTube video.
* No validation of the videoId is done here; callers are expected to pass a valid ID.
*
* Parameters:
* rel=0 — it hides related videos at the end of playback.
* modestbranding=1 — it reduces YouTube’s own branding (logo, etc.).
* controls=1 — it shows the player controls (play, pause, etc.).
*
* @param videoId 11-character YouTube video ID
* @returns Full embed URL string
*
* It simply concatenates strings. The URL is formed as:
* @example
* embedUrl("Ac_nVJASVfc")
* // → "https://www.youtube.com/embed/Ac_nVJASVfc?rel=0&modestbranding=1&controls=1"
*/
export function embedUrl(videoId: string): string {
return (
`https://www.youtube.com/embed/${videoId}` +
`?rel=0&modestbranding=1&controls=1`
);
}
/**
* Build the YouTube thumbnail URL for a video ID.
* Used to show a preview image in the list before the video loads.
*
* YouTube provides several thumbnail sizes:
* default 120×90
* "mq" → mqdefault.jpg (320×180)
* "hq" → hqdefault.jpg (480×360, good default)
* "max" → maxresdefault.jpg (1280×720, may not exist for all videos)
*
* @param videoId 11-character YouTube video ID
* @param quality "hq" (default) | "mq" | "max"
* @returns Thumbnail URL string
*/
export function thumbnailUrl(
videoId: string,
quality: "hq" | "mq" | "max" = "hq"
): string {
// The ternary expression picks the right suffix.
const suffix =
quality === "max" ? "maxresdefault" :
quality === "mq" ? "mqdefault" :
"hqdefault";
return `https://img.youtube.com/vi/${videoId}/${suffix}.jpg`;
}
app/(tabs)/videos.tsx with the test screenIt’s meant to validate the videoService module before the real UI (with a WebView player) is built in later on.
// app/(tabs)/videos.tsx
//
// VIDEOS TAB — Test screen
//
// On mount, calls loadVideos()
// Displays a loading indicator while the file is being read.
// Lists each video with its index, title, YouTube ID, embed URL, and thumbnail URL.
// React Native components:
import React, { useEffect, useState } from "react";
import {
View, // – container.
Text, // – styled text.
ScrollView, // – scrollable container.
StyleSheet, // – used to create a s object with styles (defined at the bottom).
TouchableOpacity,
ActivityIndicator, // – spinning loader.
} from "react-native";
// Service functions
import { loadVideos, embedUrl, thumbnailUrl } from "../../src/services/videoService";
// loadVideos (async loader), embedUrl, thumbnailUrl (URL builders).
import type { VideoSite } from "../../src/db/types"; // – type definition for { id: string; title: string }.
// Component: VideosScreen
// It’s the default export, so the router mounts it as the screen for the “videos” tab.
export default function VideosScreen() {
// State
// videos – array of parsed video objects. Initially empty.
const [videos, setVideos] = useState<VideoSite[]>([]);
// loading – true until the loadVideos() promise settles.
const [loading, setLoading] = useState(true);
// error – if the promise rejects, the error message is stored here (as a string).
// Normally loadVideos never throws, but the .catch is there for safety.
const [error, setError] = useState<string | null>(null);
// Effect
useEffect(() => {
// 1. Calls loadVideos() once on mount.
loadVideos()
// 2. On success, sets videos to the returned array (via setVideos).
.then(setVideos)
// 3. On failure, converts the error to a string and stores it in error.
.catch(e => setError(String(e)))
// 4. In both cases, loading is set to false (using finally).
.finally(() => setLoading(false));
// 5. deps. The empty dependency array [] ensures it runs only once after the initial render.
}, []);
// Render logic
// Loading state.
// While the file is being read, the screen shows a centered spinner and a muted “Loading videos…” message.
// This prevents the ScrollView from rendering before data is ready.
if (loading) {
return (
<View style={s.center}>
<ActivityIndicator size="large" color="#c9a84c" />
<Text style={s.muted}>Loading videos…</Text>
</View>
);
}
// Main content (when not loading)
// The rest of the component returns a <ScrollView> with several sections.
// 1. Status card
return (
<ScrollView style={s.scroll} contentContainerStyle={s.content}>
{/* Status. Changes background color based on whether error is set. */}
<View style={[s.statusCard, error ? s.statusError : s.statusOk]}>
<Text style={error ? s.textError : s.textOk}>
/* Displays either an error message or a success count (correctly pluralising “video(s)”). */
{error
? `❌ Error: ${error}`
: `✅ ${videos.length} video${videos.length !== 1 ? "s" : ""} loaded`}
</Text>
</View>
{/* 2. Video list */}
{videos.map((video, i) => (
<View key={video.id} style={s.card}>
/* Each video becomes a card containing several rows: */
{/* A. Header row: index + title */}
<View style={s.cardHeader}>
/* Displays the list position (i+1, 1‑based) and the title. */
<Text style={s.cardIndex}>{i + 1}</Text>
/* numberOfLines={2} truncates long titles with an ellipsis. */
<Text style={s.cardTitle} numberOfLines={2}>
{video.title}
</Text>
</View>
{/* B. ID row */}
<View style={s.row}>
<Text style={s.rowLabel}>ID</Text>
<Text style={s.rowValue}>{video.id}</Text>
</View>
{/* C. Embed URL row */}
<View style={s.row}>
<Text style={s.rowLabel}>Embed URL</Text>
/* Shows the generated embed URL (embedUrl(video.id)), truncated to one line (numberOfLines={1}). */
<Text style={s.rowValueSmall} numberOfLines={1}>
{embedUrl(video.id)}
</Text>
</View>
{/* D. Thumbnail URL row */}
<View style={s.row}>
<Text style={s.rowLabel}>Thumbnail</Text>
<Text style={s.rowValueSmall} numberOfLines={1}>
{thumbnailUrl(video.id)}
</Text>
</View>
</View>
))}
{/* Python comparison */}
<View style={s.hintCard}>
<Text style={s.hintTitle}>Python equivalent</Text>
<Text style={s.hintCode}>
{
"# From bible_gui.py load_videos():\n" +
"for line in VIDEOS_FILE.read_text().splitlines():\n" +
' parts = line.split(None, 1)\n' +
' videos.append({"id": parts[0], "title": parts[1]})\n\n' +
"# TypeScript equivalent:\n" +
"lines\n" +
" .map(line => line.trim())\n" +
" .filter(l => l && !l.startsWith('#'))\n" +
" .map(l => { const [id,...r]=l.split(/\\s+/); return {id,title:r.join(' ')} })"
}
</Text>
</View>
// A small footer indicating the current development stage and what comes next.
<Text style={s.chunkLabel}>
Test screen complete · Full WebView player in next posts.
</Text>
</ScrollView>
);
}
const s = StyleSheet.create({
scroll: { flex: 1, backgroundColor: "#1a1f2e" },
content: { padding: 20, paddingBottom: 40 },
center: { flex: 1, backgroundColor: "#1a1f2e",
alignItems: "center", justifyContent: "center", gap: 12 },
muted: { color: "#9ca3af", fontSize: 14 },
statusCard: { borderRadius: 10, padding: 14, marginBottom: 16,
borderWidth: 1, alignItems: "center" },
statusOk: { backgroundColor: "#1a2e1a", borderColor: "#2d4a2d" },
statusError: { backgroundColor: "#2e1a1a", borderColor: "#4a2d2d" },
textOk: { color: "#86efac", fontSize: 14, fontWeight: "600" },
textError: { color: "#f87171", fontSize: 13 },
card: { backgroundColor: "#222840", borderRadius: 10,
padding: 14, marginBottom: 12,
borderWidth: 1, borderColor: "#2d3550" },
cardHeader: { flexDirection: "row", alignItems: "flex-start",
gap: 10, marginBottom: 10 },
cardIndex: { width: 24, height: 24, borderRadius: 12,
backgroundColor: "#5b8fd4", textAlign: "center",
lineHeight: 24, fontSize: 12, color: "#fff",
fontWeight: "700", flexShrink: 0 },
cardTitle: { flex: 1, fontSize: 14, fontWeight: "600",
color: "#dde4f0", lineHeight: 20 },
row: { flexDirection: "row", alignItems: "flex-start",
paddingVertical: 4, borderTopWidth: 1,
borderTopColor: "#1a1f2e", gap: 8 },
rowLabel: { width: 72, fontSize: 10, color: "#6b7280",
fontWeight: "600", textTransform: "uppercase",
letterSpacing: 0.3, paddingTop: 2, flexShrink: 0 },
rowValue: { flex: 1, fontSize: 12, color: "#9ca3af",
fontFamily: "monospace" },
rowValueSmall: { flex: 1, fontSize: 10, color: "#4b5563",
fontFamily: "monospace" },
hintCard: { backgroundColor: "#1a1f2e", borderRadius: 8, padding: 14,
marginBottom: 16, borderWidth: 1, borderColor: "#2d3550" },
hintTitle: { fontSize: 11, color: "#6b7280", fontWeight: "600",
textTransform: "uppercase", letterSpacing: 0.5,
marginBottom: 8 },
hintCode: { fontSize: 10, color: "#4b5563", fontFamily: "monospace",
lineHeight: 16 },
chunkLabel: { fontSize: 10, color: "#4b5563", textAlign: "center",
fontFamily: "monospace" },
});
npx expo start --clear.✅ 5 videos loaded
1 Peaceful Morning Prayer Music - 1 Hour
ID Ac_nVJASVfc
Embed URL https://www.youtube.com/embed/Ac_nVJASVfc?rel=0...
Thumbnail https://img.youtube.com/vi/Ac_nVJASVfc/hqdefault.jpg
2 10 Minute Morning Prayer with Scripture
ID eYdmcZUSt0s
...