Update
This commit is contained in:
		@@ -44,35 +44,63 @@ function EpisodeList() {
 | 
			
		||||
  const [error, setError] = useState<string | null>(null);
 | 
			
		||||
  const [currentAudio, setCurrentAudio] = useState<string | null>(null);
 | 
			
		||||
  const [useDatabase, setUseDatabase] = useState(true);
 | 
			
		||||
  // Pagination state
 | 
			
		||||
  const [currentPage, setCurrentPage] = useState<number>(1);
 | 
			
		||||
  const [pageSize, setPageSize] = useState<number>(20);
 | 
			
		||||
  const [totalPages, setTotalPages] = useState<number>(1);
 | 
			
		||||
  const [totalEpisodes, setTotalEpisodes] = useState<number>(0);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    fetchEpisodes();
 | 
			
		||||
    fetchCategories();
 | 
			
		||||
  }, [useDatabase]);
 | 
			
		||||
  }, [useDatabase, currentPage, pageSize]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (searchQuery.trim()) {
 | 
			
		||||
      performSearch();
 | 
			
		||||
    } else {
 | 
			
		||||
    } else if (!useDatabase) {
 | 
			
		||||
      // Only filter locally if using XML data (non-paginated)
 | 
			
		||||
      filterEpisodesByCategory();
 | 
			
		||||
    }
 | 
			
		||||
  }, [episodes, selectedCategory, searchQuery]);
 | 
			
		||||
  
 | 
			
		||||
  // Reset to page 1 when category changes (but don't trigger if already on page 1)
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (currentPage !== 1) {
 | 
			
		||||
      setCurrentPage(1);
 | 
			
		||||
    } else {
 | 
			
		||||
      // If already on page 1, still need to refetch with new category
 | 
			
		||||
      fetchEpisodes();
 | 
			
		||||
    }
 | 
			
		||||
  }, [selectedCategory]);
 | 
			
		||||
 | 
			
		||||
  const fetchEpisodes = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      setLoading(true);
 | 
			
		||||
      setError(null);
 | 
			
		||||
 | 
			
		||||
      if (useDatabase) {
 | 
			
		||||
        // Try to fetch from database first
 | 
			
		||||
        const response = await fetch("/api/episodes-with-feed-info");
 | 
			
		||||
        // Try to fetch from database first with pagination
 | 
			
		||||
        const searchParams = new URLSearchParams({
 | 
			
		||||
          page: currentPage.toString(),
 | 
			
		||||
          limit: pageSize.toString(),
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        if (selectedCategory) {
 | 
			
		||||
          searchParams.append("category", selectedCategory);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        const response = await fetch(`/api/episodes-with-feed-info?${searchParams}`);
 | 
			
		||||
        if (!response.ok) {
 | 
			
		||||
          throw new Error("データベースからの取得に失敗しました");
 | 
			
		||||
        }
 | 
			
		||||
        const data = await response.json();
 | 
			
		||||
        
 | 
			
		||||
        // Handle paginated response
 | 
			
		||||
        if (data.episodes !== undefined) {
 | 
			
		||||
          const dbEpisodes = data.episodes || [];
 | 
			
		||||
          
 | 
			
		||||
        if (dbEpisodes.length === 0) {
 | 
			
		||||
          if (dbEpisodes.length === 0 && data.total === 0) {
 | 
			
		||||
            // Database is empty, fallback to XML
 | 
			
		||||
            console.log("Database is empty, falling back to XML parsing...");
 | 
			
		||||
            setUseDatabase(false);
 | 
			
		||||
@@ -80,6 +108,19 @@ function EpisodeList() {
 | 
			
		||||
          }
 | 
			
		||||
          
 | 
			
		||||
          setEpisodes(dbEpisodes);
 | 
			
		||||
          setTotalEpisodes(data.total || 0);
 | 
			
		||||
          setTotalPages(data.totalPages || 1);
 | 
			
		||||
          setFilteredEpisodes(dbEpisodes); // For paginated data, episodes are already filtered
 | 
			
		||||
        } else {
 | 
			
		||||
          // Fallback to non-paginated response
 | 
			
		||||
          const dbEpisodes = data.episodes || [];
 | 
			
		||||
          if (dbEpisodes.length === 0) {
 | 
			
		||||
            console.log("Database is empty, falling back to XML parsing...");
 | 
			
		||||
            setUseDatabase(false);
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          setEpisodes(dbEpisodes);
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        // Use XML parsing as primary source
 | 
			
		||||
        const response = await fetch("/api/episodes-from-xml");
 | 
			
		||||
@@ -321,6 +362,8 @@ function EpisodeList() {
 | 
			
		||||
            エピソード一覧 (
 | 
			
		||||
            {searchQuery
 | 
			
		||||
              ? `検索結果: ${filteredEpisodes.length}件`
 | 
			
		||||
              : useDatabase
 | 
			
		||||
                ? `${totalEpisodes}件中 ${Math.min((currentPage - 1) * pageSize + 1, totalEpisodes)}-${Math.min(currentPage * pageSize, totalEpisodes)}件を表示 (${currentPage}/${totalPages}ページ)`
 | 
			
		||||
                : selectedCategory
 | 
			
		||||
                  ? `${filteredEpisodes.length}/${episodes.length}件`
 | 
			
		||||
                  : `${episodes.length}件`}
 | 
			
		||||
@@ -547,6 +590,108 @@ function EpisodeList() {
 | 
			
		||||
          ))}
 | 
			
		||||
        </tbody>
 | 
			
		||||
      </table>
 | 
			
		||||
      
 | 
			
		||||
      {/* Pagination Controls - only show for database mode */}
 | 
			
		||||
      {useDatabase && totalPages > 1 && (
 | 
			
		||||
        <div
 | 
			
		||||
          style={{
 | 
			
		||||
            marginTop: "20px",
 | 
			
		||||
            display: "flex",
 | 
			
		||||
            justifyContent: "center",
 | 
			
		||||
            alignItems: "center",
 | 
			
		||||
            gap: "10px",
 | 
			
		||||
            flexWrap: "wrap",
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <button
 | 
			
		||||
            className="btn btn-secondary"
 | 
			
		||||
            onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
 | 
			
		||||
            disabled={currentPage === 1}
 | 
			
		||||
          >
 | 
			
		||||
            前へ
 | 
			
		||||
          </button>
 | 
			
		||||
          
 | 
			
		||||
          {/* Page numbers */}
 | 
			
		||||
          <div style={{ display: "flex", gap: "5px", alignItems: "center" }}>
 | 
			
		||||
            {/* First page */}
 | 
			
		||||
            {currentPage > 3 && (
 | 
			
		||||
              <>
 | 
			
		||||
                <button
 | 
			
		||||
                  className={`btn ${currentPage === 1 ? "btn-primary" : "btn-secondary"}`}
 | 
			
		||||
                  onClick={() => setCurrentPage(1)}
 | 
			
		||||
                >
 | 
			
		||||
                  1
 | 
			
		||||
                </button>
 | 
			
		||||
                {currentPage > 4 && <span>...</span>}
 | 
			
		||||
              </>
 | 
			
		||||
            )}
 | 
			
		||||
            
 | 
			
		||||
            {/* Current page and nearby pages */}
 | 
			
		||||
            {Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
 | 
			
		||||
              const pageNum = Math.max(1, currentPage - 2) + i;
 | 
			
		||||
              if (pageNum > totalPages) return null;
 | 
			
		||||
              if (pageNum < Math.max(1, currentPage - 2)) return null;
 | 
			
		||||
              
 | 
			
		||||
              return (
 | 
			
		||||
                <button
 | 
			
		||||
                  key={pageNum}
 | 
			
		||||
                  className={`btn ${currentPage === pageNum ? "btn-primary" : "btn-secondary"}`}
 | 
			
		||||
                  onClick={() => setCurrentPage(pageNum)}
 | 
			
		||||
                  style={{
 | 
			
		||||
                    minWidth: "40px",
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  {pageNum}
 | 
			
		||||
                </button>
 | 
			
		||||
              );
 | 
			
		||||
            })}
 | 
			
		||||
            
 | 
			
		||||
            {/* Last page */}
 | 
			
		||||
            {currentPage < totalPages - 2 && (
 | 
			
		||||
              <>
 | 
			
		||||
                {currentPage < totalPages - 3 && <span>...</span>}
 | 
			
		||||
                <button
 | 
			
		||||
                  className={`btn ${currentPage === totalPages ? "btn-primary" : "btn-secondary"}`}
 | 
			
		||||
                  onClick={() => setCurrentPage(totalPages)}
 | 
			
		||||
                >
 | 
			
		||||
                  {totalPages}
 | 
			
		||||
                </button>
 | 
			
		||||
              </>
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
          
 | 
			
		||||
          <button
 | 
			
		||||
            className="btn btn-secondary"
 | 
			
		||||
            onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
 | 
			
		||||
            disabled={currentPage === totalPages}
 | 
			
		||||
          >
 | 
			
		||||
            次へ
 | 
			
		||||
          </button>
 | 
			
		||||
          
 | 
			
		||||
          {/* Page size selector */}
 | 
			
		||||
          <div style={{ marginLeft: "20px", display: "flex", alignItems: "center", gap: "5px" }}>
 | 
			
		||||
            <span style={{ fontSize: "14px" }}>表示件数:</span>
 | 
			
		||||
            <select
 | 
			
		||||
              value={pageSize}
 | 
			
		||||
              onChange={(e) => {
 | 
			
		||||
                setPageSize(Number.parseInt(e.target.value, 10));
 | 
			
		||||
                setCurrentPage(1); // Reset to first page when changing page size
 | 
			
		||||
              }}
 | 
			
		||||
              style={{
 | 
			
		||||
                padding: "4px 8px",
 | 
			
		||||
                fontSize: "14px",
 | 
			
		||||
                border: "1px solid #ccc",
 | 
			
		||||
                borderRadius: "4px",
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <option value={10}>10</option>
 | 
			
		||||
              <option value={20}>20</option>
 | 
			
		||||
              <option value={50}>50</option>
 | 
			
		||||
              <option value={100}>100</option>
 | 
			
		||||
            </select>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										27
									
								
								server.ts
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								server.ts
									
									
									
									
									
								
							@@ -627,11 +627,32 @@ app.get("/api/episode-with-source/:episodeId", async (c) => {
 | 
			
		||||
 | 
			
		||||
app.get("/api/episodes-with-feed-info", async (c) => {
 | 
			
		||||
  try {
 | 
			
		||||
    const { fetchEpisodesWithFeedInfo } = await import(
 | 
			
		||||
      "./services/database.js"
 | 
			
		||||
    );
 | 
			
		||||
    const page = c.req.query("page");
 | 
			
		||||
    const limit = c.req.query("limit");
 | 
			
		||||
    const category = c.req.query("category");
 | 
			
		||||
    
 | 
			
		||||
    // If pagination parameters are provided, use paginated endpoint
 | 
			
		||||
    if (page || limit) {
 | 
			
		||||
      const { fetchEpisodesWithFeedInfoPaginated } = await import("./services/database.js");
 | 
			
		||||
      const pageNum = page ? Number.parseInt(page, 10) : 1;
 | 
			
		||||
      const limitNum = limit ? Number.parseInt(limit, 10) : 20;
 | 
			
		||||
      
 | 
			
		||||
      // Validate pagination parameters
 | 
			
		||||
      if (Number.isNaN(pageNum) || pageNum < 1) {
 | 
			
		||||
        return c.json({ error: "Invalid page number" }, 400);
 | 
			
		||||
      }
 | 
			
		||||
      if (Number.isNaN(limitNum) || limitNum < 1 || limitNum > 100) {
 | 
			
		||||
        return c.json({ error: "Invalid limit (must be between 1-100)" }, 400);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      const result = await fetchEpisodesWithFeedInfoPaginated(pageNum, limitNum, category || undefined);
 | 
			
		||||
      return c.json(result);
 | 
			
		||||
    } else {
 | 
			
		||||
      // Original behavior for backward compatibility
 | 
			
		||||
      const { fetchEpisodesWithFeedInfo } = await import("./services/database.js");
 | 
			
		||||
      const episodes = await fetchEpisodesWithFeedInfo();
 | 
			
		||||
      return c.json({ episodes });
 | 
			
		||||
    }
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error("Error fetching episodes with feed info:", error);
 | 
			
		||||
    return c.json({ error: "Failed to fetch episodes with feed info" }, 500);
 | 
			
		||||
 
 | 
			
		||||
@@ -454,6 +454,98 @@ export async function fetchEpisodesWithFeedInfo(): Promise<
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Get episodes with feed information for enhanced display (paginated)
 | 
			
		||||
export async function fetchEpisodesWithFeedInfoPaginated(
 | 
			
		||||
  page: number = 1,
 | 
			
		||||
  limit: number = 10,
 | 
			
		||||
  category?: string
 | 
			
		||||
): Promise<{ episodes: EpisodeWithFeedInfo[]; total: number; page: number; limit: number; totalPages: number }> {
 | 
			
		||||
  try {
 | 
			
		||||
    const offset = (page - 1) * limit;
 | 
			
		||||
    
 | 
			
		||||
    // Build query conditions
 | 
			
		||||
    let whereCondition = "WHERE f.active = 1";
 | 
			
		||||
    const params: any[] = [];
 | 
			
		||||
    
 | 
			
		||||
    if (category) {
 | 
			
		||||
      whereCondition += " AND e.category = ?";
 | 
			
		||||
      params.push(category);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Get total count
 | 
			
		||||
    const countStmt = db.prepare(`
 | 
			
		||||
      SELECT COUNT(*) as count 
 | 
			
		||||
      FROM episodes e
 | 
			
		||||
      JOIN articles a ON e.article_id = a.id
 | 
			
		||||
      JOIN feeds f ON a.feed_id = f.id
 | 
			
		||||
      ${whereCondition}
 | 
			
		||||
    `);
 | 
			
		||||
    const countResult = countStmt.get(...params) as { count: number };
 | 
			
		||||
    const total = countResult.count;
 | 
			
		||||
    
 | 
			
		||||
    // Get paginated episodes
 | 
			
		||||
    const episodesStmt = db.prepare(`
 | 
			
		||||
      SELECT 
 | 
			
		||||
        e.id,
 | 
			
		||||
        e.title,
 | 
			
		||||
        e.description,
 | 
			
		||||
        e.audio_path as audioPath,
 | 
			
		||||
        e.duration,
 | 
			
		||||
        e.file_size as fileSize,
 | 
			
		||||
        e.category,
 | 
			
		||||
        e.created_at as createdAt,
 | 
			
		||||
        e.article_id as articleId,
 | 
			
		||||
        a.title as articleTitle,
 | 
			
		||||
        a.link as articleLink,
 | 
			
		||||
        a.pub_date as articlePubDate,
 | 
			
		||||
        f.id as feedId,
 | 
			
		||||
        f.title as feedTitle,
 | 
			
		||||
        f.url as feedUrl,
 | 
			
		||||
        f.category as feedCategory
 | 
			
		||||
      FROM episodes e
 | 
			
		||||
      JOIN articles a ON e.article_id = a.id
 | 
			
		||||
      JOIN feeds f ON a.feed_id = f.id
 | 
			
		||||
      ${whereCondition}
 | 
			
		||||
      ORDER BY e.created_at DESC 
 | 
			
		||||
      LIMIT ? OFFSET ?
 | 
			
		||||
    `);
 | 
			
		||||
    
 | 
			
		||||
    const rows = episodesStmt.all(...params, limit, offset) as any[];
 | 
			
		||||
    
 | 
			
		||||
    const episodes = rows.map((row) => ({
 | 
			
		||||
      id: row.id,
 | 
			
		||||
      title: row.title,
 | 
			
		||||
      description: row.description,
 | 
			
		||||
      audioPath: row.audioPath,
 | 
			
		||||
      duration: row.duration,
 | 
			
		||||
      fileSize: row.fileSize,
 | 
			
		||||
      category: row.category,
 | 
			
		||||
      createdAt: row.createdAt,
 | 
			
		||||
      articleId: row.articleId,
 | 
			
		||||
      articleTitle: row.articleTitle,
 | 
			
		||||
      articleLink: row.articleLink,
 | 
			
		||||
      articlePubDate: row.articlePubDate,
 | 
			
		||||
      feedId: row.feedId,
 | 
			
		||||
      feedTitle: row.feedTitle,
 | 
			
		||||
      feedUrl: row.feedUrl,
 | 
			
		||||
      feedCategory: row.feedCategory,
 | 
			
		||||
    }));
 | 
			
		||||
    
 | 
			
		||||
    const totalPages = Math.ceil(total / limit);
 | 
			
		||||
    
 | 
			
		||||
    return {
 | 
			
		||||
      episodes,
 | 
			
		||||
      total,
 | 
			
		||||
      page,
 | 
			
		||||
      limit,
 | 
			
		||||
      totalPages
 | 
			
		||||
    };
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error("Error fetching paginated episodes with feed info:", error);
 | 
			
		||||
    throw error;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Get episodes by feed ID
 | 
			
		||||
export async function fetchEpisodesByFeedId(
 | 
			
		||||
  feedId: string,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user