Add AppImageHub.com OCS API as primary catalog source
Integrate the Pling/OCS REST API (appimagehub.com) as the primary catalog source with richer metadata than the existing appimage.github.io feed. Backend: - Add OCS API fetch with pagination, lenient JSON deserializers for loosely typed numeric fields, and non-AppImage file filtering (.dmg, .exe, etc.) - Database migration v17 adds OCS-specific columns (ocs_id, downloads, score, typename, personid, description, summary, version, tags, etc.) - Deduplicate secondary source apps against OCS entries - Shrink OCS CDN icon URLs from 770x540 to 100x100 for faster loading - Clear stale screenshot and icon caches on sync - Extract GitHub repo links from OCS HTML descriptions - Add fetch_ocs_download_files() to get all version files for an app - Resolve fresh JWT download URLs per slot at install time Detail page: - Fetch OCS download files on page open and populate install SplitButton with version dropdown (newest first, filtered for AppImage only) - Show OCS metadata: downloads, score, author, typename, tags, comments, created/updated dates, architecture, filename, file size, MD5 - Prefer ocs_description (full HTML with features/changelog) over short summary for the About section - Add html_to_description() to preserve formatting (lists, paragraphs) - Remove redundant Download link from Links section - Escape ampersands in Pango markup subtitles (categories, typename, tags) Catalog view: - OCS source syncs first as primary, appimage.github.io as secondary - Featured apps consider OCS download counts alongside GitHub stars UI: - Add pulldown-cmark for GitHub README markdown rendering in detail pages - Add build_markdown_view() widget for rendered markdown content
This commit is contained in:
@@ -88,6 +88,33 @@ pub struct SystemModification {
|
||||
pub previous_value: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum CatalogSortOrder {
|
||||
NameAsc,
|
||||
NameDesc,
|
||||
StarsDesc,
|
||||
StarsAsc,
|
||||
DownloadsDesc,
|
||||
DownloadsAsc,
|
||||
ReleaseDateDesc,
|
||||
ReleaseDateAsc,
|
||||
}
|
||||
|
||||
impl CatalogSortOrder {
|
||||
pub fn sql_clause(&self) -> &'static str {
|
||||
match self {
|
||||
Self::NameAsc => "ORDER BY name COLLATE NOCASE ASC",
|
||||
Self::NameDesc => "ORDER BY name COLLATE NOCASE DESC",
|
||||
Self::StarsDesc => "ORDER BY COALESCE(github_stars, 0) DESC, name COLLATE NOCASE ASC",
|
||||
Self::StarsAsc => "ORDER BY CASE WHEN github_stars IS NULL THEN 1 ELSE 0 END, github_stars ASC, name COLLATE NOCASE ASC",
|
||||
Self::DownloadsDesc => "ORDER BY (COALESCE(ocs_downloads, 0) + COALESCE(github_downloads, 0)) DESC, name COLLATE NOCASE ASC",
|
||||
Self::DownloadsAsc => "ORDER BY CASE WHEN ocs_downloads IS NULL AND github_downloads IS NULL THEN 1 ELSE 0 END, (COALESCE(ocs_downloads, 0) + COALESCE(github_downloads, 0)) ASC, name COLLATE NOCASE ASC",
|
||||
Self::ReleaseDateDesc => "ORDER BY COALESCE(release_date, '0000') DESC, name COLLATE NOCASE ASC",
|
||||
Self::ReleaseDateAsc => "ORDER BY CASE WHEN release_date IS NULL THEN 1 ELSE 0 END, release_date ASC, name COLLATE NOCASE ASC",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CatalogApp {
|
||||
pub id: i64,
|
||||
@@ -108,6 +135,27 @@ pub struct CatalogApp {
|
||||
pub github_enriched_at: Option<String>,
|
||||
pub github_download_url: Option<String>,
|
||||
pub github_release_assets: Option<String>,
|
||||
pub github_description: Option<String>,
|
||||
pub github_readme: Option<String>,
|
||||
// OCS (appimagehub.com) metadata
|
||||
pub ocs_id: Option<i64>,
|
||||
pub ocs_downloads: Option<i64>,
|
||||
pub ocs_score: Option<i64>,
|
||||
pub ocs_typename: Option<String>,
|
||||
pub ocs_personid: Option<String>,
|
||||
pub ocs_description: Option<String>,
|
||||
pub ocs_summary: Option<String>,
|
||||
pub ocs_version: Option<String>,
|
||||
pub ocs_tags: Option<String>,
|
||||
pub ocs_changed: Option<String>,
|
||||
pub ocs_preview_url: Option<String>,
|
||||
pub ocs_detailpage: Option<String>,
|
||||
pub ocs_created: Option<String>,
|
||||
pub ocs_downloadname: Option<String>,
|
||||
pub ocs_downloadsize: Option<i64>,
|
||||
pub ocs_arch: Option<String>,
|
||||
pub ocs_md5sum: Option<String>,
|
||||
pub ocs_comments: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -426,6 +474,18 @@ impl Database {
|
||||
self.migrate_to_v15()?;
|
||||
}
|
||||
|
||||
if current_version < 16 {
|
||||
self.migrate_to_v16()?;
|
||||
}
|
||||
|
||||
if current_version < 17 {
|
||||
self.migrate_to_v17()?;
|
||||
}
|
||||
|
||||
if current_version < 18 {
|
||||
self.migrate_to_v18()?;
|
||||
}
|
||||
|
||||
// Ensure all expected columns exist (repairs DBs where a migration
|
||||
// was updated after it had already run on this database)
|
||||
self.ensure_columns()?;
|
||||
@@ -930,6 +990,68 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn migrate_to_v16(&self) -> SqlResult<()> {
|
||||
let new_columns = [
|
||||
"github_description TEXT",
|
||||
"github_readme TEXT",
|
||||
];
|
||||
for col in &new_columns {
|
||||
let sql = format!("ALTER TABLE catalog_apps ADD COLUMN {}", col);
|
||||
self.conn.execute(&sql, []).ok();
|
||||
}
|
||||
self.conn.execute(
|
||||
"UPDATE schema_version SET version = ?1",
|
||||
params![16],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn migrate_to_v17(&self) -> SqlResult<()> {
|
||||
let new_columns = [
|
||||
"ocs_id INTEGER",
|
||||
"ocs_downloads INTEGER",
|
||||
"ocs_score INTEGER",
|
||||
"ocs_typename TEXT",
|
||||
"ocs_personid TEXT",
|
||||
"ocs_description TEXT",
|
||||
"ocs_summary TEXT",
|
||||
"ocs_version TEXT",
|
||||
"ocs_tags TEXT",
|
||||
"ocs_changed TEXT",
|
||||
"ocs_preview_url TEXT",
|
||||
];
|
||||
for col in &new_columns {
|
||||
let sql = format!("ALTER TABLE catalog_apps ADD COLUMN {}", col);
|
||||
self.conn.execute(&sql, []).ok();
|
||||
}
|
||||
self.conn.execute(
|
||||
"UPDATE schema_version SET version = ?1",
|
||||
params![17],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn migrate_to_v18(&self) -> SqlResult<()> {
|
||||
let new_columns = [
|
||||
"ocs_detailpage TEXT",
|
||||
"ocs_created TEXT",
|
||||
"ocs_downloadname TEXT",
|
||||
"ocs_downloadsize INTEGER",
|
||||
"ocs_arch TEXT",
|
||||
"ocs_md5sum TEXT",
|
||||
"ocs_comments INTEGER",
|
||||
];
|
||||
for col in &new_columns {
|
||||
let sql = format!("ALTER TABLE catalog_apps ADD COLUMN {}", col);
|
||||
self.conn.execute(&sql, []).ok();
|
||||
}
|
||||
self.conn.execute(
|
||||
"UPDATE schema_version SET version = ?1",
|
||||
params![18],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn upsert_appimage(
|
||||
&self,
|
||||
path: &str,
|
||||
@@ -1095,6 +1217,57 @@ impl Database {
|
||||
previous_version_path, source_url, autostart, startup_wm_class,
|
||||
verification_status, first_run_prompted, system_wide, is_portable, mount_point";
|
||||
|
||||
const CATALOG_APP_COLUMNS: &str =
|
||||
"id, name, description, categories, download_url, icon_url, homepage, license, screenshots,
|
||||
github_owner, github_repo, github_stars, github_downloads, latest_version, release_date,
|
||||
github_enriched_at, github_download_url, github_release_assets, github_description, github_readme,
|
||||
ocs_id, ocs_downloads, ocs_score, ocs_typename, ocs_personid, ocs_description, ocs_summary,
|
||||
ocs_version, ocs_tags, ocs_changed, ocs_preview_url,
|
||||
ocs_detailpage, ocs_created, ocs_downloadname, ocs_downloadsize, ocs_arch, ocs_md5sum, ocs_comments";
|
||||
|
||||
fn catalog_app_from_row(row: &rusqlite::Row) -> rusqlite::Result<CatalogApp> {
|
||||
Ok(CatalogApp {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
description: row.get(2)?,
|
||||
categories: row.get(3)?,
|
||||
download_url: row.get(4)?,
|
||||
icon_url: row.get(5)?,
|
||||
homepage: row.get(6)?,
|
||||
license: row.get(7)?,
|
||||
screenshots: row.get(8)?,
|
||||
github_owner: row.get(9)?,
|
||||
github_repo: row.get(10)?,
|
||||
github_stars: row.get(11)?,
|
||||
github_downloads: row.get(12)?,
|
||||
latest_version: row.get(13)?,
|
||||
release_date: row.get(14)?,
|
||||
github_enriched_at: row.get(15)?,
|
||||
github_download_url: row.get(16)?,
|
||||
github_release_assets: row.get(17)?,
|
||||
github_description: row.get(18)?,
|
||||
github_readme: row.get(19)?,
|
||||
ocs_id: row.get(20).unwrap_or(None),
|
||||
ocs_downloads: row.get(21).unwrap_or(None),
|
||||
ocs_score: row.get(22).unwrap_or(None),
|
||||
ocs_typename: row.get(23).unwrap_or(None),
|
||||
ocs_personid: row.get(24).unwrap_or(None),
|
||||
ocs_description: row.get(25).unwrap_or(None),
|
||||
ocs_summary: row.get(26).unwrap_or(None),
|
||||
ocs_version: row.get(27).unwrap_or(None),
|
||||
ocs_tags: row.get(28).unwrap_or(None),
|
||||
ocs_changed: row.get(29).unwrap_or(None),
|
||||
ocs_preview_url: row.get(30).unwrap_or(None),
|
||||
ocs_detailpage: row.get(31).unwrap_or(None),
|
||||
ocs_created: row.get(32).unwrap_or(None),
|
||||
ocs_downloadname: row.get(33).unwrap_or(None),
|
||||
ocs_downloadsize: row.get(34).unwrap_or(None),
|
||||
ocs_arch: row.get(35).unwrap_or(None),
|
||||
ocs_md5sum: row.get(36).unwrap_or(None),
|
||||
ocs_comments: row.get(37).unwrap_or(None),
|
||||
})
|
||||
}
|
||||
|
||||
fn row_to_record(row: &rusqlite::Row) -> rusqlite::Result<AppImageRecord> {
|
||||
Ok(AppImageRecord {
|
||||
id: row.get(0)?,
|
||||
@@ -2159,16 +2332,16 @@ impl Database {
|
||||
query: &str,
|
||||
category: Option<&str>,
|
||||
limit: i32,
|
||||
sort: CatalogSortOrder,
|
||||
) -> SqlResult<Vec<CatalogApp>> {
|
||||
let mut sql = String::from(
|
||||
"SELECT id, name, description, categories, download_url, icon_url, homepage, license, screenshots,
|
||||
github_owner, github_repo, github_stars, github_downloads, latest_version, release_date, github_enriched_at, github_download_url, github_release_assets
|
||||
FROM catalog_apps WHERE 1=1"
|
||||
let mut sql = format!(
|
||||
"SELECT {} FROM catalog_apps WHERE 1=1",
|
||||
Self::CATALOG_APP_COLUMNS
|
||||
);
|
||||
let mut params_list: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
|
||||
|
||||
if !query.is_empty() {
|
||||
sql.push_str(" AND (name LIKE ?1 OR description LIKE ?1)");
|
||||
sql.push_str(" AND (name LIKE ?1 OR description LIKE ?1 OR ocs_summary LIKE ?1 OR ocs_description LIKE ?1)");
|
||||
params_list.push(Box::new(format!("%{}%", query)));
|
||||
}
|
||||
|
||||
@@ -2178,34 +2351,13 @@ impl Database {
|
||||
params_list.push(Box::new(format!("%{}%", cat)));
|
||||
}
|
||||
|
||||
sql.push_str(&format!(" ORDER BY name LIMIT {}", limit));
|
||||
sql.push_str(&format!(" {} LIMIT {}", sort.sql_clause(), limit));
|
||||
|
||||
let params_refs: Vec<&dyn rusqlite::types::ToSql> =
|
||||
params_list.iter().map(|p| p.as_ref()).collect();
|
||||
|
||||
let mut stmt = self.conn.prepare(&sql)?;
|
||||
let rows = stmt.query_map(params_refs.as_slice(), |row| {
|
||||
Ok(CatalogApp {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
description: row.get(2)?,
|
||||
categories: row.get(3)?,
|
||||
download_url: row.get(4)?,
|
||||
icon_url: row.get(5)?,
|
||||
homepage: row.get(6)?,
|
||||
license: row.get(7)?,
|
||||
screenshots: row.get(8)?,
|
||||
github_owner: row.get(9)?,
|
||||
github_repo: row.get(10)?,
|
||||
github_stars: row.get(11)?,
|
||||
github_downloads: row.get(12)?,
|
||||
latest_version: row.get(13)?,
|
||||
release_date: row.get(14)?,
|
||||
github_enriched_at: row.get(15)?,
|
||||
github_download_url: row.get(16)?,
|
||||
github_release_assets: row.get(17)?,
|
||||
})
|
||||
})?;
|
||||
let rows = stmt.query_map(params_refs.as_slice(), Self::catalog_app_from_row)?;
|
||||
|
||||
let mut results = Vec::new();
|
||||
for row in rows {
|
||||
@@ -2215,34 +2367,11 @@ impl Database {
|
||||
}
|
||||
|
||||
pub fn get_catalog_app(&self, id: i64) -> SqlResult<Option<CatalogApp>> {
|
||||
let result = self.conn.query_row(
|
||||
"SELECT id, name, description, categories, download_url, icon_url, homepage, license, screenshots,
|
||||
github_owner, github_repo, github_stars, github_downloads, latest_version, release_date, github_enriched_at, github_download_url, github_release_assets
|
||||
FROM catalog_apps WHERE id = ?1",
|
||||
params![id],
|
||||
|row| {
|
||||
Ok(CatalogApp {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
description: row.get(2)?,
|
||||
categories: row.get(3)?,
|
||||
download_url: row.get(4)?,
|
||||
icon_url: row.get(5)?,
|
||||
homepage: row.get(6)?,
|
||||
license: row.get(7)?,
|
||||
screenshots: row.get(8)?,
|
||||
github_owner: row.get(9)?,
|
||||
github_repo: row.get(10)?,
|
||||
github_stars: row.get(11)?,
|
||||
github_downloads: row.get(12)?,
|
||||
latest_version: row.get(13)?,
|
||||
release_date: row.get(14)?,
|
||||
github_enriched_at: row.get(15)?,
|
||||
github_download_url: row.get(16)?,
|
||||
github_release_assets: row.get(17)?,
|
||||
})
|
||||
},
|
||||
let sql = format!(
|
||||
"SELECT {} FROM catalog_apps WHERE id = ?1",
|
||||
Self::CATALOG_APP_COLUMNS
|
||||
);
|
||||
let result = self.conn.query_row(&sql, params![id], Self::catalog_app_from_row);
|
||||
match result {
|
||||
Ok(app) => Ok(Some(app)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
@@ -2250,8 +2379,9 @@ impl Database {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get featured catalog apps. Apps with GitHub stars sort first (by stars desc),
|
||||
/// then unenriched apps get a deterministic shuffle that rotates every 15 minutes.
|
||||
/// Get featured catalog apps. Apps with enrichment data (stars or OCS downloads)
|
||||
/// sort first by combined popularity, then unenriched apps get a deterministic
|
||||
/// shuffle that rotates every 15 minutes.
|
||||
pub fn get_featured_catalog_apps(&self, limit: i32) -> SqlResult<Vec<CatalogApp>> {
|
||||
// Time seed rotates every 15 minutes (900 seconds)
|
||||
let time_seed = std::time::SystemTime::now()
|
||||
@@ -2259,46 +2389,31 @@ impl Database {
|
||||
.unwrap_or_default()
|
||||
.as_secs() / 900;
|
||||
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, name, description, categories, download_url, icon_url, homepage, license, screenshots,
|
||||
github_owner, github_repo, github_stars, github_downloads, latest_version, release_date, github_enriched_at, github_download_url, github_release_assets
|
||||
FROM catalog_apps
|
||||
let sql = format!(
|
||||
"SELECT {} FROM catalog_apps
|
||||
WHERE icon_url IS NOT NULL AND icon_url != ''
|
||||
AND description IS NOT NULL AND description != ''
|
||||
AND screenshots IS NOT NULL AND screenshots != ''"
|
||||
)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok(CatalogApp {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
description: row.get(2)?,
|
||||
categories: row.get(3)?,
|
||||
download_url: row.get(4)?,
|
||||
icon_url: row.get(5)?,
|
||||
homepage: row.get(6)?,
|
||||
license: row.get(7)?,
|
||||
screenshots: row.get(8)?,
|
||||
github_owner: row.get(9)?,
|
||||
github_repo: row.get(10)?,
|
||||
github_stars: row.get(11)?,
|
||||
github_downloads: row.get(12)?,
|
||||
latest_version: row.get(13)?,
|
||||
release_date: row.get(14)?,
|
||||
github_enriched_at: row.get(15)?,
|
||||
github_download_url: row.get(16)?,
|
||||
github_release_assets: row.get(17)?,
|
||||
})
|
||||
})?;
|
||||
AND (description IS NOT NULL AND description != ''
|
||||
OR ocs_summary IS NOT NULL AND ocs_summary != '')
|
||||
AND (screenshots IS NOT NULL AND screenshots != ''
|
||||
OR ocs_preview_url IS NOT NULL AND ocs_preview_url != '')",
|
||||
Self::CATALOG_APP_COLUMNS
|
||||
);
|
||||
let mut stmt = self.conn.prepare(&sql)?;
|
||||
let rows = stmt.query_map([], Self::catalog_app_from_row)?;
|
||||
let mut apps: Vec<CatalogApp> = rows.collect::<SqlResult<Vec<_>>>()?;
|
||||
|
||||
// Enriched apps (with stars) sort first by stars descending,
|
||||
// unenriched apps get the deterministic shuffle after them
|
||||
// Sort by combined popularity: OCS downloads + GitHub stars.
|
||||
// Apps with any enrichment sort first, then deterministic shuffle.
|
||||
apps.sort_by(|a, b| {
|
||||
match (a.github_stars, b.github_stars) {
|
||||
(Some(sa), Some(sb)) => sb.cmp(&sa),
|
||||
(Some(_), None) => std::cmp::Ordering::Less,
|
||||
(None, Some(_)) => std::cmp::Ordering::Greater,
|
||||
(None, None) => {
|
||||
let a_pop = a.ocs_downloads.unwrap_or(0) + a.github_stars.unwrap_or(0);
|
||||
let b_pop = b.ocs_downloads.unwrap_or(0) + b.github_stars.unwrap_or(0);
|
||||
let a_enriched = a_pop > 0;
|
||||
let b_enriched = b_pop > 0;
|
||||
match (a_enriched, b_enriched) {
|
||||
(true, false) => std::cmp::Ordering::Less,
|
||||
(false, true) => std::cmp::Ordering::Greater,
|
||||
(true, true) => b_pop.cmp(&a_pop),
|
||||
(false, false) => {
|
||||
let ha = (a.id as u64 ^ time_seed).wrapping_mul(0x517cc1b727220a95);
|
||||
let hb = (b.id as u64 ^ time_seed).wrapping_mul(0x517cc1b727220a95);
|
||||
ha.cmp(&hb)
|
||||
@@ -2369,6 +2484,97 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn insert_ocs_catalog_app(
|
||||
&self,
|
||||
source_id: i64,
|
||||
name: &str,
|
||||
ocs_id: i64,
|
||||
description: Option<&str>,
|
||||
summary: Option<&str>,
|
||||
categories: Option<&str>,
|
||||
version: Option<&str>,
|
||||
download_url: &str,
|
||||
icon_url: Option<&str>,
|
||||
homepage: Option<&str>,
|
||||
screenshots: Option<&str>,
|
||||
ocs_downloads: Option<i64>,
|
||||
ocs_score: Option<i64>,
|
||||
ocs_typename: Option<&str>,
|
||||
ocs_personid: Option<&str>,
|
||||
ocs_tags: Option<&str>,
|
||||
ocs_changed: Option<&str>,
|
||||
ocs_preview_url: Option<&str>,
|
||||
license: Option<&str>,
|
||||
ocs_detailpage: Option<&str>,
|
||||
ocs_created: Option<&str>,
|
||||
ocs_downloadname: Option<&str>,
|
||||
ocs_downloadsize: Option<i64>,
|
||||
ocs_arch: Option<&str>,
|
||||
ocs_md5sum: Option<&str>,
|
||||
ocs_comments: Option<i64>,
|
||||
) -> SqlResult<()> {
|
||||
self.conn.execute(
|
||||
"INSERT INTO catalog_apps
|
||||
(source_id, name, description, categories, latest_version, download_url, icon_url, homepage,
|
||||
screenshots, license, cached_at,
|
||||
ocs_id, ocs_downloads, ocs_score, ocs_typename, ocs_personid, ocs_description, ocs_summary,
|
||||
ocs_version, ocs_tags, ocs_changed, ocs_preview_url,
|
||||
ocs_detailpage, ocs_created, ocs_downloadname, ocs_downloadsize, ocs_arch, ocs_md5sum, ocs_comments)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, datetime('now'),
|
||||
?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21,
|
||||
?22, ?23, ?24, ?25, ?26, ?27, ?28)
|
||||
ON CONFLICT(source_id, name) DO UPDATE SET
|
||||
description = excluded.description,
|
||||
categories = excluded.categories,
|
||||
latest_version = excluded.latest_version,
|
||||
download_url = excluded.download_url,
|
||||
icon_url = excluded.icon_url,
|
||||
homepage = excluded.homepage,
|
||||
screenshots = excluded.screenshots,
|
||||
license = excluded.license,
|
||||
cached_at = datetime('now'),
|
||||
ocs_id = excluded.ocs_id,
|
||||
ocs_downloads = excluded.ocs_downloads,
|
||||
ocs_score = excluded.ocs_score,
|
||||
ocs_typename = excluded.ocs_typename,
|
||||
ocs_personid = excluded.ocs_personid,
|
||||
ocs_description = excluded.ocs_description,
|
||||
ocs_summary = excluded.ocs_summary,
|
||||
ocs_version = excluded.ocs_version,
|
||||
ocs_tags = excluded.ocs_tags,
|
||||
ocs_changed = excluded.ocs_changed,
|
||||
ocs_preview_url = excluded.ocs_preview_url,
|
||||
ocs_detailpage = excluded.ocs_detailpage,
|
||||
ocs_created = excluded.ocs_created,
|
||||
ocs_downloadname = excluded.ocs_downloadname,
|
||||
ocs_downloadsize = excluded.ocs_downloadsize,
|
||||
ocs_arch = excluded.ocs_arch,
|
||||
ocs_md5sum = excluded.ocs_md5sum,
|
||||
ocs_comments = excluded.ocs_comments",
|
||||
params![
|
||||
source_id, name, summary, categories, version, download_url, icon_url, homepage,
|
||||
screenshots, license,
|
||||
ocs_id, ocs_downloads, ocs_score, ocs_typename, ocs_personid, description, summary,
|
||||
version, ocs_tags, ocs_changed, ocs_preview_url,
|
||||
ocs_detailpage, ocs_created, ocs_downloadname, ocs_downloadsize, ocs_arch, ocs_md5sum, ocs_comments
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all app names for a given source (used for deduplication).
|
||||
pub fn get_catalog_app_names_for_source(&self, source_id: i64) -> SqlResult<std::collections::HashSet<String>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT LOWER(name) FROM catalog_apps WHERE source_id = ?1"
|
||||
)?;
|
||||
let rows = stmt.query_map(params![source_id], |row| row.get::<_, String>(0))?;
|
||||
let mut names = std::collections::HashSet::new();
|
||||
for row in rows {
|
||||
names.insert(row?);
|
||||
}
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
pub fn search_catalog_apps(&self, query: &str) -> SqlResult<Vec<CatalogAppRecord>> {
|
||||
let pattern = format!("%{}%", query);
|
||||
let mut stmt = self.conn.prepare(
|
||||
@@ -2453,10 +2659,11 @@ impl Database {
|
||||
app_id: i64,
|
||||
stars: i64,
|
||||
pushed_at: Option<&str>,
|
||||
description: Option<&str>,
|
||||
) -> SqlResult<()> {
|
||||
self.conn.execute(
|
||||
"UPDATE catalog_apps SET github_stars = ?2, github_enriched_at = datetime('now') WHERE id = ?1",
|
||||
params![app_id, stars],
|
||||
"UPDATE catalog_apps SET github_stars = ?2, github_description = COALESCE(?3, github_description), github_enriched_at = datetime('now') WHERE id = ?1",
|
||||
params![app_id, stars, description],
|
||||
)?;
|
||||
// Store pushed_at in release_date if no release info yet
|
||||
if let Some(pushed) = pushed_at {
|
||||
@@ -2492,36 +2699,15 @@ impl Database {
|
||||
}
|
||||
|
||||
pub fn get_unenriched_catalog_apps(&self, limit: i32) -> SqlResult<Vec<CatalogApp>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, name, description, categories, download_url, icon_url, homepage, license, screenshots,
|
||||
github_owner, github_repo, github_stars, github_downloads, latest_version, release_date, github_enriched_at, github_download_url, github_release_assets
|
||||
FROM catalog_apps
|
||||
let sql = format!(
|
||||
"SELECT {} FROM catalog_apps
|
||||
WHERE github_owner IS NOT NULL AND github_enriched_at IS NULL
|
||||
ORDER BY id
|
||||
LIMIT ?1"
|
||||
)?;
|
||||
let rows = stmt.query_map(params![limit], |row| {
|
||||
Ok(CatalogApp {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
description: row.get(2)?,
|
||||
categories: row.get(3)?,
|
||||
download_url: row.get(4)?,
|
||||
icon_url: row.get(5)?,
|
||||
homepage: row.get(6)?,
|
||||
license: row.get(7)?,
|
||||
screenshots: row.get(8)?,
|
||||
github_owner: row.get(9)?,
|
||||
github_repo: row.get(10)?,
|
||||
github_stars: row.get(11)?,
|
||||
github_downloads: row.get(12)?,
|
||||
latest_version: row.get(13)?,
|
||||
release_date: row.get(14)?,
|
||||
github_enriched_at: row.get(15)?,
|
||||
github_download_url: row.get(16)?,
|
||||
github_release_assets: row.get(17)?,
|
||||
})
|
||||
})?;
|
||||
LIMIT ?1",
|
||||
Self::CATALOG_APP_COLUMNS
|
||||
);
|
||||
let mut stmt = self.conn.prepare(&sql)?;
|
||||
let rows = stmt.query_map(params![limit], Self::catalog_app_from_row)?;
|
||||
rows.collect()
|
||||
}
|
||||
|
||||
@@ -2538,6 +2724,18 @@ impl Database {
|
||||
)?;
|
||||
Ok((enriched, total_with_github))
|
||||
}
|
||||
|
||||
pub fn update_catalog_app_readme(
|
||||
&self,
|
||||
app_id: i64,
|
||||
readme: &str,
|
||||
) -> SqlResult<()> {
|
||||
self.conn.execute(
|
||||
"UPDATE catalog_apps SET github_readme = ?2 WHERE id = ?1",
|
||||
params![app_id, readme],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -2708,7 +2906,7 @@ mod tests {
|
||||
[],
|
||||
|row| row.get(0),
|
||||
).unwrap();
|
||||
assert_eq!(version, 15);
|
||||
assert_eq!(version, 18);
|
||||
|
||||
// All tables that should exist after the full v1-v7 migration chain
|
||||
let expected_tables = [
|
||||
|
||||
Reference in New Issue
Block a user