@tricoteuses/senat 2.21.2 → 2.21.4

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.
@@ -97,7 +97,7 @@ async function writeMatchArtifacts(args) {
97
97
  if (finalTxt)
98
98
  await fsp.writeFile(path.join(ctx.baseDir, "finalplayer.nvs"), finalTxt, "utf-8");
99
99
  }
100
- async function processGroupedReunion(agenda, session, dataDir) {
100
+ async function processGroupedReunion(agenda, session, dataDir, lastByVideo) {
101
101
  // 1) GuardRails
102
102
  if (shouldSkipAgenda(agenda))
103
103
  return;
@@ -137,18 +137,30 @@ async function processGroupedReunion(agenda, session, dataDir) {
137
137
  session: ctx.session,
138
138
  options,
139
139
  writeIfChanged,
140
+ lastByVideo, // NEW
141
+ getAgendaSegmentTimecodes,
142
+ buildSenatVodMasterM3u8FromNvs,
143
+ });
144
+ await processBisIfNeeded({
145
+ agenda,
146
+ secondBest,
147
+ ctx,
148
+ skipDownload,
149
+ options,
150
+ lastByVideo,
151
+ writeIfChanged,
152
+ processOneReunionMatch,
140
153
  getAgendaSegmentTimecodes,
141
154
  buildSenatVodMasterM3u8FromNvs,
142
155
  });
143
- // 4) Optional BIS
144
- await processBisIfNeeded({ agenda, secondBest, ctx, skipDownload, options });
145
156
  }
146
157
  async function processAll(dataDir, sessions) {
147
158
  console.log("Process all Agendas and fetch video's url");
148
159
  for (const session of sessions) {
160
+ const lastByVideo = new Map();
149
161
  for (const { item: agenda } of iterLoadSenatAgendas(dataDir, session)) {
150
162
  try {
151
- await processGroupedReunion(agenda, session, dataDir);
163
+ await processGroupedReunion(agenda, session, dataDir, lastByVideo);
152
164
  }
153
165
  catch (e) {
154
166
  console.error(`[error] ${agenda?.uid ?? "unknown-uid"}:`, e?.message || e);
@@ -103,7 +103,7 @@ export function parseDataNvs(nvs) {
103
103
  }
104
104
  export function buildSenatVodMasterM3u8FromNvs(nvsText) {
105
105
  // serverfiles://senat/2025/10/encoder10_20251022084451_2.mp4
106
- const m = nvsText.match(/serverfiles:\/\/senat\/(\d{4})\/(\d{2})\/(encoder\d+)_([0-9]{14})/i);
106
+ const m = nvsText.match(/serverfiles:\/\/senat\/(\d{4})\/(\d{2})\/(encoder\d+)_([0-9]{13,14})(?:_[0-9]+)?\.mp4/i);
107
107
  if (!m)
108
108
  return null;
109
109
  const [, yyyy, mm, encoder, stamp] = m;
@@ -1,5 +1,5 @@
1
1
  import { Reunion } from "../types/agenda";
2
- import { BestMatch, MatchContext } from "./types";
2
+ import { BestMatch, LastForVideo, MatchContext } from "./types";
3
3
  import { CommandLineOptions } from "command-line-args";
4
4
  export declare function processOneReunionMatch(args: {
5
5
  agenda: Reunion;
@@ -8,6 +8,11 @@ export declare function processOneReunionMatch(args: {
8
8
  session: number;
9
9
  options: Record<string, any>;
10
10
  writeIfChanged: (p: string, content: string) => Promise<void>;
11
+ lastByVideo: Map<string, {
12
+ agendaUid: string;
13
+ agendaJsonPath: string;
14
+ start: number;
15
+ }>;
11
16
  getAgendaSegmentTimecodes: (dataNvs: string, finalNvs: string, agendaKey: string) => {
12
17
  start: number;
13
18
  end: number | null;
@@ -20,5 +25,26 @@ export declare function processBisIfNeeded(args: {
20
25
  ctx: MatchContext;
21
26
  skipDownload: boolean;
22
27
  options: CommandLineOptions;
28
+ lastByVideo: Map<string, LastForVideo>;
29
+ writeIfChanged: (p: string, content: string) => Promise<void>;
30
+ processOneReunionMatch: (args: {
31
+ agenda: Reunion;
32
+ baseDir: string;
33
+ dataDir: string;
34
+ session: number;
35
+ options: Record<string, any>;
36
+ writeIfChanged: (p: string, content: string) => Promise<void>;
37
+ lastByVideo: Map<string, LastForVideo>;
38
+ getAgendaSegmentTimecodes: (dataNvs: string, finalNvs: string, agendaKey: string) => {
39
+ start: number;
40
+ end: number | null;
41
+ } | null;
42
+ buildSenatVodMasterM3u8FromNvs: (dataNvs: string) => string | null;
43
+ }) => Promise<void>;
44
+ getAgendaSegmentTimecodes: (dataNvs: string, finalNvs: string, agendaKey: string) => {
45
+ start: number;
46
+ end: number | null;
47
+ } | null;
48
+ buildSenatVodMasterM3u8FromNvs: (dataNvs: string) => string | null;
23
49
  }): Promise<void>;
24
50
  export declare function writeIfChanged(p: string, content: string): Promise<void>;
@@ -5,9 +5,8 @@ import { fetchText } from "./search";
5
5
  import fs from "fs-extra";
6
6
  import fsp from "fs/promises";
7
7
  import path from "path";
8
- import { getAgendaSegmentTimecodes, buildSenatVodMasterM3u8FromNvs } from "../utils/nvs-parsing";
9
8
  export async function processOneReunionMatch(args) {
10
- const { agenda, baseDir, dataDir, session, options, writeIfChanged, getAgendaSegmentTimecodes, buildSenatVodMasterM3u8FromNvs, } = args;
9
+ const { agenda, baseDir, dataDir, session, options, writeIfChanged, lastByVideo, getAgendaSegmentTimecodes, buildSenatVodMasterM3u8FromNvs, } = args;
11
10
  const reunionUid = agenda.uid;
12
11
  let dataTxt;
13
12
  let finalTxt;
@@ -25,18 +24,40 @@ export async function processOneReunionMatch(args) {
25
24
  return;
26
25
  }
27
26
  const agendaJsonPath = path.join(dataDir, AGENDA_FOLDER, DATA_TRANSFORMED_FOLDER, String(session), `${agenda.uid}.json`);
27
+ // Ensure it exists first.
28
+ if (!(await fs.pathExists(agendaJsonPath))) {
29
+ console.warn(`[warn] agenda file not found: ${agendaJsonPath}`);
30
+ return;
31
+ }
28
32
  let timecodeDebutVideo = null;
29
33
  let timecodeFinVideo = null;
30
34
  const agendaKey = agenda.titre || agenda.objet || "";
31
35
  const seg = getAgendaSegmentTimecodes(dataTxt, finalTxt, agendaKey);
32
36
  if (seg) {
33
37
  timecodeDebutVideo = seg.start;
34
- timecodeFinVideo = seg.end;
38
+ timecodeFinVideo = null; // keep open by default
35
39
  }
36
- if (!(await fs.pathExists(agendaJsonPath))) {
37
- console.warn(`[warn] agenda file not found: ${agendaJsonPath}`);
38
- return;
40
+ // 1) If we have a start timecode, close the previous agenda for this SAME master
41
+ if (timecodeDebutVideo != null) {
42
+ const prev = lastByVideo.get(master);
43
+ if (prev && prev.agendaJsonPath !== agendaJsonPath) {
44
+ // micro-safety: do not close with an earlier timecode
45
+ if (timecodeDebutVideo <= prev.start) {
46
+ console.warn(`[warn] timecode order inversion on same video: ` +
47
+ `prev=${prev.agendaUid}(${prev.start}s) -> cur=${agenda.uid}(${timecodeDebutVideo}s). ` +
48
+ `Skip closing prev to avoid negative segment.`);
49
+ }
50
+ else {
51
+ await patchAgendaTimecodeFin({
52
+ agendaJsonPath: prev.agendaJsonPath,
53
+ timecodeFinVideo: timecodeDebutVideo,
54
+ writeIfChanged,
55
+ });
56
+ }
57
+ }
58
+ lastByVideo.set(master, { agendaUid: agenda.uid, agendaJsonPath, start: timecodeDebutVideo });
39
59
  }
60
+ // 2) Update current agenda JSON with urlVideo (+ start/end if any)
40
61
  const raw = await fsp.readFile(agendaJsonPath, "utf-8");
41
62
  let obj;
42
63
  try {
@@ -49,7 +70,10 @@ export async function processOneReunionMatch(args) {
49
70
  const next = { ...obj, urlVideo: master, startTime: agenda.startTime };
50
71
  if (timecodeDebutVideo != null) {
51
72
  next.timecodeDebutVideo = timecodeDebutVideo;
52
- next.timecodeFinVideo = timecodeFinVideo ?? undefined;
73
+ if (timecodeFinVideo != null)
74
+ next.timecodeFinVideo = timecodeFinVideo;
75
+ else
76
+ delete next.timecodeFinVideo;
53
77
  }
54
78
  await writeIfChanged(agendaJsonPath, JSON.stringify(next, null, 2));
55
79
  if (!options["silent"]) {
@@ -58,7 +82,7 @@ export async function processOneReunionMatch(args) {
58
82
  }
59
83
  }
60
84
  export async function processBisIfNeeded(args) {
61
- const { agenda, secondBest, ctx, skipDownload, options } = args;
85
+ const { agenda, secondBest, ctx, skipDownload, options, lastByVideo, writeIfChanged, processOneReunionMatch, getAgendaSegmentTimecodes, buildSenatVodMasterM3u8FromNvs, } = args;
62
86
  if (skipDownload)
63
87
  return;
64
88
  if (!secondBest)
@@ -83,6 +107,7 @@ export async function processBisIfNeeded(args) {
83
107
  session: ctx.session,
84
108
  options,
85
109
  writeIfChanged,
110
+ lastByVideo,
86
111
  getAgendaSegmentTimecodes,
87
112
  buildSenatVodMasterM3u8FromNvs,
88
113
  });
@@ -128,3 +153,19 @@ export async function writeIfChanged(p, content) {
128
153
  }
129
154
  await fsp.writeFile(p, content, "utf-8");
130
155
  }
156
+ async function patchAgendaTimecodeFin(args) {
157
+ const { agendaJsonPath, timecodeFinVideo, writeIfChanged } = args;
158
+ if (!(await fs.pathExists(agendaJsonPath)))
159
+ return;
160
+ const raw = await fsp.readFile(agendaJsonPath, "utf-8");
161
+ let obj;
162
+ try {
163
+ obj = JSON.parse(raw);
164
+ }
165
+ catch {
166
+ console.warn(`[warn] invalid JSON in ${agendaJsonPath}`);
167
+ return;
168
+ }
169
+ const next = { ...obj, timecodeFinVideo };
170
+ await writeIfChanged(agendaJsonPath, JSON.stringify(next, null, 2));
171
+ }
@@ -68,3 +68,8 @@ export type MatchContext = {
68
68
  reunionUid: string;
69
69
  agendaTs: number | null;
70
70
  };
71
+ export type LastForVideo = {
72
+ agendaUid: string;
73
+ agendaJsonPath: string;
74
+ start: number;
75
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tricoteuses/senat",
3
- "version": "2.21.2",
3
+ "version": "2.21.4",
4
4
  "description": "Handle French Sénat's open data",
5
5
  "keywords": [
6
6
  "France",