daleel-mcp 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/db.d.ts +1 -0
- package/dist/db.js +5 -10
- package/dist/index.js +127 -54
- package/dist/services.d.ts +34 -0
- package/dist/services.js +137 -0
- package/package.json +1 -1
package/dist/db.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ declare class DaleelDiscoveryEngine {
|
|
|
15
15
|
constructor();
|
|
16
16
|
private detectAppDataDir;
|
|
17
17
|
private initAppDb;
|
|
18
|
+
getAppDb(): any;
|
|
18
19
|
getAppDataPath(): string | null;
|
|
19
20
|
getInstalledPackages(): InstalledPackage[];
|
|
20
21
|
openPackageDb(localFileName: string): any | null;
|
package/dist/db.js
CHANGED
|
@@ -13,12 +13,10 @@ class DaleelDiscoveryEngine {
|
|
|
13
13
|
this.initAppDb();
|
|
14
14
|
}
|
|
15
15
|
detectAppDataDir() {
|
|
16
|
-
// 1. Explicit environment variable
|
|
17
16
|
if (process.env.DALEEL_APP_DATA_DIR && fs.existsSync(process.env.DALEEL_APP_DATA_DIR)) {
|
|
18
17
|
this.appDataDir = process.env.DALEEL_APP_DATA_DIR;
|
|
19
18
|
return;
|
|
20
19
|
}
|
|
21
|
-
// 2. Platform default paths for Daleel Scholar (Tauri identifier: com.daleel.scholar)
|
|
22
20
|
const platform = os.platform();
|
|
23
21
|
let candidate = "";
|
|
24
22
|
if (platform === "win32") {
|
|
@@ -34,13 +32,6 @@ class DaleelDiscoveryEngine {
|
|
|
34
32
|
if (fs.existsSync(candidate)) {
|
|
35
33
|
this.appDataDir = candidate;
|
|
36
34
|
}
|
|
37
|
-
else {
|
|
38
|
-
// Fallback check in case running inside the repo
|
|
39
|
-
const devPath = path.resolve(process.cwd(), "../../content-source/ur");
|
|
40
|
-
if (fs.existsSync(devPath)) {
|
|
41
|
-
this.appDataDir = devPath;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
35
|
}
|
|
45
36
|
initAppDb() {
|
|
46
37
|
if (!this.appDataDir)
|
|
@@ -48,7 +39,8 @@ class DaleelDiscoveryEngine {
|
|
|
48
39
|
const appDbPath = path.join(this.appDataDir, "app.db");
|
|
49
40
|
if (fs.existsSync(appDbPath)) {
|
|
50
41
|
try {
|
|
51
|
-
|
|
42
|
+
// Open with read-write so MCP can sync timeline markers to Scholar App
|
|
43
|
+
this.appDb = new DatabaseSync(appDbPath);
|
|
52
44
|
}
|
|
53
45
|
catch (err) {
|
|
54
46
|
console.error("Failed to open app.db:", err);
|
|
@@ -64,6 +56,9 @@ class DaleelDiscoveryEngine {
|
|
|
64
56
|
}
|
|
65
57
|
}
|
|
66
58
|
}
|
|
59
|
+
getAppDb() {
|
|
60
|
+
return this.appDb;
|
|
61
|
+
}
|
|
67
62
|
getAppDataPath() {
|
|
68
63
|
return this.appDataDir;
|
|
69
64
|
}
|
package/dist/index.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
-
import { listInstalledBooks, searchInBook,
|
|
5
|
+
import { listInstalledBooks, searchInBook, getBookPage, verifyCitationAgainstDb, syncTimelineToScholarApp, getAppTimelineMarkers, validateScholarVideoAccess, getCurrentScholarProject, } from "./services.js";
|
|
6
6
|
import { discoveryEngine } from "./db.js";
|
|
7
7
|
const server = new Server({
|
|
8
|
-
name: "daleel-
|
|
9
|
-
version: "2.
|
|
8
|
+
name: "daleel-scholar-mcp",
|
|
9
|
+
version: "2.2.0",
|
|
10
10
|
}, {
|
|
11
11
|
capabilities: {
|
|
12
12
|
tools: {},
|
|
@@ -16,68 +16,126 @@ const server = new Server({
|
|
|
16
16
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
17
17
|
return {
|
|
18
18
|
tools: [
|
|
19
|
+
{
|
|
20
|
+
name: "get_current_scholar_project",
|
|
21
|
+
description: "Checks which YouTube video is currently active or recently opened in the user's Daleel Scholar Desktop (Tauri) App. Use this first to auto-detect what the scholar is working on.",
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: "object",
|
|
24
|
+
properties: {},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "validate_scholar_video_access",
|
|
29
|
+
description: "SECURITY GATEKEEPER: Verifies whether a given YouTube video ID exists in the scholar's authorized Daleel workspace. Rejects unknown or unauthorized videos.",
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
youtube_video_id: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "YouTube 11-char Video ID (e.g. dQw4w9WgXcQ)",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
required: ["youtube_video_id"],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
19
41
|
{
|
|
20
42
|
name: "list_installed_books",
|
|
21
|
-
description: "List all Islamic books, Hadith collections, and
|
|
43
|
+
description: "List all Islamic books, Hadith collections, and Tafseer works currently downloaded in Daleel Scholar App.",
|
|
22
44
|
inputSchema: {
|
|
23
45
|
type: "object",
|
|
24
46
|
properties: {},
|
|
25
47
|
},
|
|
26
48
|
},
|
|
27
49
|
{
|
|
28
|
-
name: "
|
|
29
|
-
description: "
|
|
50
|
+
name: "verify_citation_against_db",
|
|
51
|
+
description: "STRICT AUTHENTICITY: Verifies a religious claim or reference recited in a video by strictly checking Daleel SQLite database. Returns verified verbatim text or reports unverified.",
|
|
30
52
|
inputSchema: {
|
|
31
53
|
type: "object",
|
|
32
54
|
properties: {
|
|
33
55
|
book_identifier: {
|
|
34
56
|
type: "string",
|
|
35
|
-
description: "Book
|
|
57
|
+
description: "Book title or ID (e.g. 'احکام و مسائل' or 'fiqh')",
|
|
36
58
|
},
|
|
37
|
-
|
|
59
|
+
search_phrase: {
|
|
38
60
|
type: "string",
|
|
39
|
-
description: "
|
|
61
|
+
description: "Specific Arabic or Urdu words mentioned in video",
|
|
40
62
|
},
|
|
41
|
-
|
|
63
|
+
timestamp_seconds: {
|
|
42
64
|
type: "number",
|
|
43
|
-
description: "
|
|
65
|
+
description: "Exact second timestamp in video",
|
|
66
|
+
},
|
|
67
|
+
speaker_claim: {
|
|
68
|
+
type: "string",
|
|
69
|
+
description: "What the speaker claimed in the video",
|
|
44
70
|
},
|
|
45
71
|
},
|
|
46
|
-
required: ["book_identifier", "
|
|
72
|
+
required: ["book_identifier", "search_phrase", "timestamp_seconds", "speaker_claim"],
|
|
47
73
|
},
|
|
48
74
|
},
|
|
49
75
|
{
|
|
50
|
-
name: "
|
|
51
|
-
description: "
|
|
76
|
+
name: "sync_timeline_to_scholar_app",
|
|
77
|
+
description: "Pushes verified citations and timestamps directly into Daleel Scholar Desktop (Tauri) App database so the user sees markers on the video timeline editor visually. Requires video to be registered in Scholar workspace.",
|
|
52
78
|
inputSchema: {
|
|
53
79
|
type: "object",
|
|
54
80
|
properties: {
|
|
55
|
-
|
|
81
|
+
youtube_video_id: {
|
|
56
82
|
type: "string",
|
|
57
|
-
description: "
|
|
83
|
+
description: "YouTube 11-char Video ID",
|
|
58
84
|
},
|
|
59
|
-
|
|
60
|
-
type: "
|
|
61
|
-
description: "
|
|
85
|
+
video_title: {
|
|
86
|
+
type: "string",
|
|
87
|
+
description: "Title of the YouTube video",
|
|
62
88
|
},
|
|
89
|
+
markers: {
|
|
90
|
+
type: "array",
|
|
91
|
+
description: "List of verified markers to place on the timeline",
|
|
92
|
+
items: {
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: {
|
|
95
|
+
timestampSeconds: { type: "number" },
|
|
96
|
+
referenceTitle: { type: "string" },
|
|
97
|
+
exactText: { type: "string", description: "Verbatim text retrieved from Daleel DB" },
|
|
98
|
+
pageOrHadithNo: { type: "string" },
|
|
99
|
+
},
|
|
100
|
+
required: ["timestampSeconds", "referenceTitle", "exactText"],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
required: ["youtube_video_id", "video_title", "markers"],
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "get_video_timeline_markers",
|
|
109
|
+
description: "Fetch existing timeline markers saved in Daleel Scholar App for a given video.",
|
|
110
|
+
inputSchema: {
|
|
111
|
+
type: "object",
|
|
112
|
+
properties: {
|
|
113
|
+
youtube_video_id: { type: "string" },
|
|
114
|
+
},
|
|
115
|
+
required: ["youtube_video_id"],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "search_in_book",
|
|
120
|
+
description: "Search for specific text or keywords inside a downloaded book in Daleel App.",
|
|
121
|
+
inputSchema: {
|
|
122
|
+
type: "object",
|
|
123
|
+
properties: {
|
|
124
|
+
book_identifier: { type: "string" },
|
|
125
|
+
query: { type: "string" },
|
|
126
|
+
limit: { type: "number" },
|
|
63
127
|
},
|
|
64
|
-
required: ["query"],
|
|
128
|
+
required: ["book_identifier", "query"],
|
|
65
129
|
},
|
|
66
130
|
},
|
|
67
131
|
{
|
|
68
132
|
name: "get_book_page",
|
|
69
|
-
description: "Fetch the exact page content and footnotes from a
|
|
133
|
+
description: "Fetch the exact page content and footnotes from a book by page number.",
|
|
70
134
|
inputSchema: {
|
|
71
135
|
type: "object",
|
|
72
136
|
properties: {
|
|
73
|
-
book_identifier: {
|
|
74
|
-
|
|
75
|
-
description: "Book ID, title, or filename",
|
|
76
|
-
},
|
|
77
|
-
page_number: {
|
|
78
|
-
type: "number",
|
|
79
|
-
description: "Exact page number (ID) to retrieve",
|
|
80
|
-
},
|
|
137
|
+
book_identifier: { type: "string" },
|
|
138
|
+
page_number: { type: "number" },
|
|
81
139
|
},
|
|
82
140
|
required: ["book_identifier", "page_number"],
|
|
83
141
|
},
|
|
@@ -90,6 +148,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
90
148
|
const { name, arguments: args } = request.params;
|
|
91
149
|
try {
|
|
92
150
|
switch (name) {
|
|
151
|
+
case "get_current_scholar_project": {
|
|
152
|
+
const project = getCurrentScholarProject();
|
|
153
|
+
return {
|
|
154
|
+
content: [
|
|
155
|
+
{
|
|
156
|
+
type: "text",
|
|
157
|
+
text: project
|
|
158
|
+
? JSON.stringify(project, null, 2)
|
|
159
|
+
: JSON.stringify({ isRegisteredInScholarApp: false, message: "No active video found in Daleel Scholar App. Please open a video in the desktop app first." }),
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
case "validate_scholar_video_access": {
|
|
165
|
+
const { youtube_video_id } = args;
|
|
166
|
+
const validation = validateScholarVideoAccess(youtube_video_id);
|
|
167
|
+
return {
|
|
168
|
+
content: [{ type: "text", text: JSON.stringify(validation, null, 2) }],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
93
171
|
case "list_installed_books": {
|
|
94
172
|
const books = listInstalledBooks();
|
|
95
173
|
const appPath = discoveryEngine.getAppDataPath();
|
|
@@ -110,38 +188,33 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
110
188
|
const { book_identifier, query, limit } = args;
|
|
111
189
|
const results = searchInBook(book_identifier, query, limit || 5);
|
|
112
190
|
return {
|
|
113
|
-
content: [
|
|
114
|
-
{
|
|
115
|
-
type: "text",
|
|
116
|
-
text: results.length
|
|
117
|
-
? JSON.stringify(results, null, 2)
|
|
118
|
-
: `No occurrences of "${query}" found in ${book_identifier}.`,
|
|
119
|
-
},
|
|
120
|
-
],
|
|
191
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
121
192
|
};
|
|
122
193
|
}
|
|
123
|
-
case "
|
|
124
|
-
const {
|
|
125
|
-
const
|
|
194
|
+
case "verify_citation_against_db": {
|
|
195
|
+
const { book_identifier, search_phrase, timestamp_seconds, speaker_claim } = args;
|
|
196
|
+
const result = verifyCitationAgainstDb(book_identifier, search_phrase, timestamp_seconds, speaker_claim);
|
|
126
197
|
return {
|
|
127
|
-
content: [
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
],
|
|
198
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
case "sync_timeline_to_scholar_app": {
|
|
202
|
+
const { youtube_video_id, video_title, markers } = args;
|
|
203
|
+
const res = syncTimelineToScholarApp(youtube_video_id, video_title, markers);
|
|
204
|
+
return {
|
|
205
|
+
content: [{ type: "text", text: JSON.stringify(res, null, 2) }],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
case "get_video_timeline_markers": {
|
|
209
|
+
const { youtube_video_id } = args;
|
|
210
|
+
const res = getAppTimelineMarkers(youtube_video_id);
|
|
211
|
+
return {
|
|
212
|
+
content: [{ type: "text", text: JSON.stringify(res, null, 2) }],
|
|
135
213
|
};
|
|
136
214
|
}
|
|
137
215
|
case "get_book_page": {
|
|
138
216
|
const { book_identifier, page_number } = args;
|
|
139
217
|
const res = getBookPage(book_identifier, page_number);
|
|
140
|
-
if (!res) {
|
|
141
|
-
return {
|
|
142
|
-
content: [{ type: "text", text: `Page ${page_number} not found in ${book_identifier}.` }],
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
218
|
return {
|
|
146
219
|
content: [{ type: "text", text: JSON.stringify(res, null, 2) }],
|
|
147
220
|
};
|
|
@@ -160,7 +233,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
160
233
|
async function main() {
|
|
161
234
|
const transport = new StdioServerTransport();
|
|
162
235
|
await server.connect(transport);
|
|
163
|
-
console.error("Daleel
|
|
236
|
+
console.error("Daleel Scholar Tauri-MCP Sync Server running on stdio");
|
|
164
237
|
}
|
|
165
238
|
main().catch((err) => {
|
|
166
239
|
console.error("Fatal error:", err);
|
package/dist/services.d.ts
CHANGED
|
@@ -10,3 +10,37 @@ export declare function listInstalledBooks(): InstalledPackage[];
|
|
|
10
10
|
export declare function searchInBook(packageIdOrFile: string, query: string, limit?: number): BookSearchResult[];
|
|
11
11
|
export declare function searchAllInstalledBooks(query: string, limit?: number): BookSearchResult[];
|
|
12
12
|
export declare function getBookPage(packageIdOrFile: string, pageNumber: number): any;
|
|
13
|
+
export interface ScholarProjectValidation {
|
|
14
|
+
isRegisteredInScholarApp: boolean;
|
|
15
|
+
youtubeVideoId: string;
|
|
16
|
+
videoTitle?: string;
|
|
17
|
+
durationSeconds?: number;
|
|
18
|
+
existingMarkersCount: number;
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function validateScholarVideoAccess(youtubeVideoId: string): ScholarProjectValidation;
|
|
22
|
+
export declare function getCurrentScholarProject(): ScholarProjectValidation | null;
|
|
23
|
+
export interface VerifiedCitationResult {
|
|
24
|
+
isVerified: boolean;
|
|
25
|
+
bookTitle?: string;
|
|
26
|
+
pageNumber?: number;
|
|
27
|
+
exactDbText?: string;
|
|
28
|
+
footnote?: string;
|
|
29
|
+
timestampSeconds: number;
|
|
30
|
+
speakerClaim: string;
|
|
31
|
+
verificationStatus: "AUTHENTIC_MATCH_FOUND" | "NO_EXACT_MATCH_IN_LOCAL_DB";
|
|
32
|
+
}
|
|
33
|
+
export declare function verifyCitationAgainstDb(bookIdentifier: string, searchPhrase: string, timestampSeconds: number, speakerClaim: string): VerifiedCitationResult;
|
|
34
|
+
export declare function syncTimelineToScholarApp(youtubeVideoId: string, videoTitle: string, markers: Array<{
|
|
35
|
+
timestampSeconds: number;
|
|
36
|
+
referenceTitle: string;
|
|
37
|
+
exactText: string;
|
|
38
|
+
pageOrHadithNo?: string | number;
|
|
39
|
+
}>): {
|
|
40
|
+
success: boolean;
|
|
41
|
+
youtubeVideoId: string;
|
|
42
|
+
videoTitle: string;
|
|
43
|
+
markersAdded: number;
|
|
44
|
+
message: string;
|
|
45
|
+
};
|
|
46
|
+
export declare function getAppTimelineMarkers(youtubeVideoId: string): any;
|
package/dist/services.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { discoveryEngine } from "./db.js";
|
|
2
|
+
import crypto from "crypto";
|
|
2
3
|
export function listInstalledBooks() {
|
|
3
4
|
return discoveryEngine.getInstalledPackages();
|
|
4
5
|
}
|
|
@@ -85,3 +86,139 @@ export function getBookPage(packageIdOrFile, pageNumber) {
|
|
|
85
86
|
footnote: row.fnotes,
|
|
86
87
|
};
|
|
87
88
|
}
|
|
89
|
+
export function validateScholarVideoAccess(youtubeVideoId) {
|
|
90
|
+
const appDb = discoveryEngine.getAppDb();
|
|
91
|
+
if (!appDb) {
|
|
92
|
+
throw new Error("Daleel Scholar App database (app.db) not accessible. Please ensure Daleel Scholar App is installed.");
|
|
93
|
+
}
|
|
94
|
+
const draftStmt = appDb.prepare(`
|
|
95
|
+
SELECT youtube_video_id, video_title, duration_seconds
|
|
96
|
+
FROM timeline_drafts
|
|
97
|
+
WHERE youtube_video_id = ?
|
|
98
|
+
LIMIT 1
|
|
99
|
+
`);
|
|
100
|
+
const draft = draftStmt.get(youtubeVideoId);
|
|
101
|
+
if (!draft) {
|
|
102
|
+
return {
|
|
103
|
+
isRegisteredInScholarApp: false,
|
|
104
|
+
youtubeVideoId,
|
|
105
|
+
existingMarkersCount: 0,
|
|
106
|
+
message: `Access Denied: Video "${youtubeVideoId}" is not registered in your Daleel Scholar App. Please open or link this video inside Daleel Scholar App first before asking AI to verify citations.`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const countStmt = appDb.prepare(`
|
|
110
|
+
SELECT COUNT(*) as count
|
|
111
|
+
FROM timeline_markers
|
|
112
|
+
WHERE youtube_video_id = ?
|
|
113
|
+
`);
|
|
114
|
+
const countRow = countStmt.get(youtubeVideoId);
|
|
115
|
+
return {
|
|
116
|
+
isRegisteredInScholarApp: true,
|
|
117
|
+
youtubeVideoId: draft.youtube_video_id,
|
|
118
|
+
videoTitle: draft.video_title,
|
|
119
|
+
durationSeconds: draft.duration_seconds,
|
|
120
|
+
existingMarkersCount: countRow?.count ?? 0,
|
|
121
|
+
message: `Access Verified: Video "${draft.video_title}" is active in Daleel Scholar workspace.`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
export function getCurrentScholarProject() {
|
|
125
|
+
const appDb = discoveryEngine.getAppDb();
|
|
126
|
+
if (!appDb)
|
|
127
|
+
return null;
|
|
128
|
+
try {
|
|
129
|
+
const stmt = appDb.prepare(`
|
|
130
|
+
SELECT youtube_video_id, video_title, duration_seconds
|
|
131
|
+
FROM timeline_drafts
|
|
132
|
+
ORDER BY updated_at DESC
|
|
133
|
+
LIMIT 1
|
|
134
|
+
`);
|
|
135
|
+
const row = stmt.get();
|
|
136
|
+
if (!row)
|
|
137
|
+
return null;
|
|
138
|
+
const countStmt = appDb.prepare(`
|
|
139
|
+
SELECT COUNT(*) as count FROM timeline_markers WHERE youtube_video_id = ?
|
|
140
|
+
`);
|
|
141
|
+
const countRow = countStmt.get(row.youtube_video_id);
|
|
142
|
+
return {
|
|
143
|
+
isRegisteredInScholarApp: true,
|
|
144
|
+
youtubeVideoId: row.youtube_video_id,
|
|
145
|
+
videoTitle: row.video_title,
|
|
146
|
+
durationSeconds: row.duration_seconds,
|
|
147
|
+
existingMarkersCount: countRow?.count ?? 0,
|
|
148
|
+
message: `Active Video in Scholar App: "${row.video_title}"`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
export function verifyCitationAgainstDb(bookIdentifier, searchPhrase, timestampSeconds, speakerClaim) {
|
|
156
|
+
try {
|
|
157
|
+
const matches = searchInBook(bookIdentifier, searchPhrase, 1);
|
|
158
|
+
if (matches.length > 0) {
|
|
159
|
+
const best = matches[0];
|
|
160
|
+
return {
|
|
161
|
+
isVerified: true,
|
|
162
|
+
bookTitle: best.bookTitle,
|
|
163
|
+
pageNumber: best.pageNumber,
|
|
164
|
+
exactDbText: best.content,
|
|
165
|
+
footnote: best.footnote,
|
|
166
|
+
timestampSeconds,
|
|
167
|
+
speakerClaim,
|
|
168
|
+
verificationStatus: "AUTHENTIC_MATCH_FOUND",
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
// book not found or search failed
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
isVerified: false,
|
|
177
|
+
timestampSeconds,
|
|
178
|
+
speakerClaim,
|
|
179
|
+
verificationStatus: "NO_EXACT_MATCH_IN_LOCAL_DB",
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
export function syncTimelineToScholarApp(youtubeVideoId, videoTitle, markers) {
|
|
183
|
+
// First gatekeeper check: video must be valid
|
|
184
|
+
const validation = validateScholarVideoAccess(youtubeVideoId);
|
|
185
|
+
if (!validation.isRegisteredInScholarApp) {
|
|
186
|
+
throw new Error(validation.message);
|
|
187
|
+
}
|
|
188
|
+
const appDb = discoveryEngine.getAppDb();
|
|
189
|
+
if (!appDb) {
|
|
190
|
+
throw new Error("Daleel Scholar App database (app.db) not accessible.");
|
|
191
|
+
}
|
|
192
|
+
const insertMarker = appDb.prepare(`
|
|
193
|
+
INSERT INTO timeline_markers (id, youtube_video_id, timestamp_seconds, note, created_at, updated_at)
|
|
194
|
+
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
195
|
+
`);
|
|
196
|
+
let addedCount = 0;
|
|
197
|
+
for (const m of markers) {
|
|
198
|
+
const markerId = crypto.randomUUID();
|
|
199
|
+
const formattedNote = `[Verified Daleel Citation]\nBook: ${m.referenceTitle}\nRef/Page: ${m.pageOrHadithNo || 'N/A'}\nVerbatim Text:\n"${m.exactText.trim()}"`;
|
|
200
|
+
insertMarker.run(markerId, youtubeVideoId, m.timestampSeconds, formattedNote);
|
|
201
|
+
addedCount++;
|
|
202
|
+
}
|
|
203
|
+
// Update draft timestamp
|
|
204
|
+
appDb.prepare(`UPDATE timeline_drafts SET updated_at = CURRENT_TIMESTAMP WHERE youtube_video_id = ?`).run(youtubeVideoId);
|
|
205
|
+
return {
|
|
206
|
+
success: true,
|
|
207
|
+
youtubeVideoId,
|
|
208
|
+
videoTitle,
|
|
209
|
+
markersAdded: addedCount,
|
|
210
|
+
message: "Timeline markers successfully synchronized with Daleel Scholar Desktop (Tauri).",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
export function getAppTimelineMarkers(youtubeVideoId) {
|
|
214
|
+
const appDb = discoveryEngine.getAppDb();
|
|
215
|
+
if (!appDb)
|
|
216
|
+
return [];
|
|
217
|
+
const stmt = appDb.prepare(`
|
|
218
|
+
SELECT id, youtube_video_id, timestamp_seconds, note, created_at
|
|
219
|
+
FROM timeline_markers
|
|
220
|
+
WHERE youtube_video_id = ?
|
|
221
|
+
ORDER BY timestamp_seconds ASC
|
|
222
|
+
`);
|
|
223
|
+
return stmt.all(youtubeVideoId);
|
|
224
|
+
}
|