• R/O
  • HTTP
  • SSH
  • HTTPS

Commit

Tags
No Tags

Frequently used words (click to add to your profile)

javac++androidlinuxc#windowsobjective-ccocoa誰得qtpythonphprubygameguibathyscaphec計画中(planning stage)翻訳omegatframeworktwitterdomtestvb.netdirectxゲームエンジンbtronarduinopreviewer

CLI interface to medialist (fossil mirror)


Commit MetaInfo

Revisionf7608093601b06e33accab594840260433a9dfe2 (tree)
Time2022-01-18 19:19:47
Authormio <stigma@disr...>
Commitermio

Log Message

replace "handle_show" with ml_fetch_all and some manually handling.

FossilOrigin-Name: d18348bffba05ffe429f7a23e698ccfdc04539a8be35e65cbb9f789d94f31f66

Change Summary

Incremental Difference

--- a/Makefile
+++ b/Makefile
@@ -27,15 +27,13 @@ endif
2727
2828 all: medialist-cli
2929
30-medialist-cli: configparser.o main.o medialist.o show.o update.o \
31- util.o xdg.o
30+medialist-cli: configparser.o main.o medialist.o update.o util.o xdg.o
3231 $(DC) $(__dof__) medialist-cli configparser.o main.o medialist.o \
33- show.o update.o util.o xdg.o
32+ update.o util.o xdg.o
3433
3534 configparser.o: configparser.d
3635 main.o: main.d
3736 medialist.o: medialist.d
38-show.o: show.d
3937 update.o: update.d
4038 util.o: util.d
4139 xdg.o: xdg.d
@@ -52,8 +50,8 @@ uninstall:
5250 rm -f $(DESTDIR)$(bindir)/medialist-cli
5351 rm -f $(DESTDIR)$(man1dir)/medialist-cli.1.gz
5452
55-unittest: configparser-u.o medialist-u.o \
56- show-u.o tap-u.o update-u.o util-u.o xdg-u.o unittests-u.o
53+unittest: configparser-u.o medialist-u.o tap-u.o update-u.o util-u.o \
54+ xdg-u.o unittests-u.o
5755 $(DC) $(DTESTFLAGS) $(__dof__) medialist-cli-unittest $^
5856
5957 configparser-u.o: configparser.d
@@ -62,9 +60,6 @@ configparser-u.o: configparser.d
6260 medialist-u.o: medialist.d
6361 $(DC) $(DTESTFLAGS) -c $(__dof__) $@ $<
6462
65-show-u.o: show.d
66- $(DC) $(DTESTFLAGS) -c $(__dof__) $@ $<
67-
6863 tap-u.o: tap.d
6964 $(DC) $(DTESTFLAGS) -c $(__dof__) $@ $<
7065
--- a/main.d
+++ b/main.d
@@ -18,6 +18,7 @@
1818 */
1919 import core.stdc.stdlib : EXIT_FAILURE, EXIT_SUCCESS;
2020
21+import std.algorithm.searching;
2122 import std.file;
2223 import std.path;
2324 import std.stdio;
@@ -25,7 +26,6 @@ import std.string;
2526
2627 import configparser : ConfigParser;
2728 import medialist;
28-import show : handle_show;
2929 import update : handle_update;
3030 import util : expandEnvironmentVariables;
3131 import xdg;
@@ -85,7 +85,7 @@ private MLError verifyDelete(MediaList* ml, string[] args)
8585 if (0 == args.length) {
8686 writef("Are you sure you want to delete the list \"%s\"? ([yes]/no) ",
8787 ml.listName);
88- string res = io.readln().strip().toLower();
88+ string res = readln().strip().toLower();
8989
9090 if ("" == res || "yes" == res) {
9191 response = ml_send_command(ml, MLCommand.delete_, []);
@@ -119,6 +119,68 @@ private MLError verifyDelete(MediaList* ml, string[] args)
119119 return response;
120120 }
121121
122+bool showList(MediaList* list, string[] args)
123+{
124+ bool numbered = false;
125+ bool showHeaders = false;
126+
127+ foreach(ref arg; args) {
128+ if ("-n" == arg || "--numbered" == arg)
129+ numbered = true;
130+
131+ if ("--show-headers" == arg)
132+ showHeaders = true;
133+ }
134+
135+ if (true == showHeaders) {
136+ MediaListHeader[] headers = ml_fetch_headers(list);
137+ string title;
138+ string progress;
139+ string status;
140+
141+ foreach(size_t idx, const ref header; headers) {
142+ switch(header.tsvName.toLower) {
143+ case "title":
144+ title = (header.humanFriendlyName == "")
145+ ? header.tsvName
146+ : header.humanFriendlyName;
147+ break;
148+ case "progress":
149+ progress = (header.humanFriendlyName is null)
150+ ? header.tsvName
151+ : header.humanFriendlyName;
152+ break;
153+ case "status":
154+ status = (header.humanFriendlyName is null)
155+ ? header.tsvName
156+ : header.humanFriendlyName;
157+ break;
158+ default:
159+ break;
160+ }
161+ }
162+
163+ writefln("%s => %s => %s", title, status, progress);
164+ /* 8 = " => " * 2 */
165+ size_t lineLength = title.length + status.length + progress.length + 8;
166+ for (size_t i = 0; i < lineLength; i++) {
167+ write("=");
168+ }
169+ write("\n");
170+ stdout.flush();
171+ }
172+
173+ MediaListItem[] items = ml_fetch_all(list);
174+ foreach(size_t idx, const ref item; items) {
175+ if (true == numbered)
176+ writef("%d: ", idx + 1);
177+
178+ writefln("%s => %s => %s", item.title, item.status, item.progress);
179+ }
180+
181+ return true;
182+}
183+
122184 int main(string[] args) {
123185 string config_dir = buildPath(configDir(), "medialist");
124186 string data_dir = buildPath(dataDir(), "medialist");
@@ -225,8 +287,8 @@ version(appimage) {
225287 }
226288 success = (res == MLError.success);
227289 break;
228- case "show": // CLI: <list> [--numbered] [--show-headers]
229- success = handle_show(program_name, args[2..$], data_dir);
290+ case "show": // CLI: [--numbered] [--show-headers]
291+ success = showList(ml, args[3..$]);
230292 break;
231293 case "delete": // CLI: <list> [<id...>]
232294 if (shouldVerifyDelete)
--- a/medialist.d
+++ b/medialist.d
@@ -34,6 +34,26 @@ struct MediaList
3434 bool isOpen = false;
3535 }
3636
37+struct MediaListItem
38+{
39+ string title;
40+ string progress;
41+ string status;
42+ /* start_date */
43+ /* end_date */
44+ /* last_updated */
45+ bool isValid;
46+}
47+
48+struct MediaListHeader
49+{
50+ string tsvName = null;
51+ string humanFriendlyName = null;
52+ size_t column;
53+}
54+
55+MediaListItem[] ml_fetch_items(MediaList *list, int ids...);
56+
3757 enum MLCommand
3858 {
3959 /**
@@ -59,6 +79,7 @@ enum MLError
5979 invalidArgs,
6080 fileDoesNotExist,
6181 fileAlreadyOpen,
82+ itemNotFound,
6283 }
6384
6485 MediaList* ml_open_list(string filePath)
@@ -99,6 +120,182 @@ void ml_free_list(MediaList* list)
99120 GC.free(list);
100121 }
101122
123+MediaListHeader[] ml_fetch_headers(MediaList* list)
124+{
125+ if (true == list.isOpen)
126+ throw new Exception("List '" ~ list.listName ~ "' is already open for modification");
127+
128+ File listFile = File(list.filePath);
129+
130+ list.isOpen = true;
131+ scope(exit) list.isOpen = false;
132+
133+ MediaListHeader[] headers;
134+ string[string] configurations;
135+
136+ string line;
137+
138+ while ((line = listFile.readln()) !is null) {
139+ if (line.length == 0)
140+ continue;
141+
142+ /* Header line, which we will parse now */
143+ if ('#' != line[0])
144+ break;
145+
146+ /*
147+ * The minimum mTSV configuration line is:
148+ * #k=v
149+ */
150+ if (line.length < 4)
151+ continue;
152+
153+ /* mTSV configuration requires there to be NO space */
154+ if (' ' != line[1])
155+ continue;
156+
157+ string[] sections = line[1..$].strip().split("=");
158+
159+ /* XXX: Should we error here? */
160+ if (sections.length < 2)
161+ continue;
162+
163+ configurations[sections[0]] = sections[1];
164+ }
165+
166+ if (line is null)
167+ throw new Exception("No header line found");
168+
169+ string[] sections = line.strip().split("\t");
170+ foreach(size_t idx, ref string section; sections) {
171+ if (section in configurations)
172+ headers ~= MediaListHeader(section, configurations[section], idx);
173+ else
174+ headers ~= MediaListHeader(section, null, idx);
175+ }
176+
177+ return headers;
178+}
179+
180+MLError ml_fetch_item(MediaList* list, size_t id, MediaListItem* item)
181+{
182+ if (true == list.isOpen)
183+ return MLError.fileAlreadyOpen;
184+
185+ list.isOpen = true;
186+
187+ File listFile = File(list.filePath);
188+
189+ string line;
190+ size_t currentLine = 0;
191+ bool found = false;
192+
193+ while ((line = listFile.readln()) !is null) {
194+ if (0 >= line.length)
195+ continue;
196+
197+ if ('#' == line[0])
198+ continue;
199+
200+ if (id == currentLine) {
201+ found = true;
202+ break;
203+ }
204+ currentLine += 1;
205+ }
206+
207+ if (false == found)
208+ return MLError.itemNotFound;
209+
210+ int[2][6] headerPositions = _ml_get_header_positions(list);
211+
212+ string[] sections = line.strip().split("\t");
213+ size_t titleIndex = 0;
214+ size_t progressIndex = 0;
215+ size_t statusIndex = 0;
216+
217+ foreach(size_t idx, int[2] header; headerPositions) {
218+ switch(header[0]) {
219+ case MLHeaders.title:
220+ titleIndex = header[1];
221+ break;
222+ case MLHeaders.progress:
223+ progressIndex = header[1];
224+ break;
225+ case MLHeaders.status:
226+ statusIndex = header[1];
227+ break;
228+ default:
229+ break;
230+ }
231+ }
232+
233+ MediaListItem newItem = MediaListItem(sections[titleIndex],
234+ sections[progressIndex], sections[statusIndex], true);
235+
236+ *item = newItem;
237+
238+ list.isOpen = false;
239+
240+ return MLError.success;
241+}
242+
243+MediaListItem[] ml_fetch_all(MediaList* list)
244+{
245+ if (list.isOpen)
246+ return null;
247+
248+ File listFile = File(list.filePath);
249+ list.isOpen = true;
250+
251+ string line;
252+ bool pastHeader = false;
253+ MediaListItem[] items;
254+
255+ int[2][6] headerPositions = _ml_get_header_positions(list);
256+ size_t titleIndex = 0;
257+ size_t progressIndex = 0;
258+ size_t statusIndex = 0;
259+
260+ foreach(ref header; headerPositions) {
261+ switch (header[0]) {
262+ case MLHeaders.title:
263+ titleIndex = header[1];
264+ break;
265+ case MLHeaders.progress:
266+ progressIndex = header[1];
267+ break;
268+ case MLHeaders.status:
269+ statusIndex = header[1];
270+ break;
271+ default:
272+ break;
273+ }
274+ }
275+
276+ while ((line = listFile.readln()) !is null) {
277+ if (line.length == 0)
278+ continue;
279+
280+ if (line[0] == '#')
281+ continue;
282+
283+ if (false == pastHeader) {
284+ pastHeader = true;
285+ continue;
286+ }
287+
288+ string[] sections = line.strip().split("\t");
289+
290+ items ~= MediaListItem(sections[titleIndex], sections[progressIndex],
291+ sections[statusIndex], true);
292+ }
293+
294+ list.isOpen = false;
295+
296+ return items;
297+}
298+
102299 MLError ml_send_command(MediaList* list, MLCommand command, string[] args)
103300 {
104301 MLError res;
@@ -379,6 +576,10 @@ public void runMediaListUnitTests()
379576 "Can add a new item with status but no progress.");
380577 ok(unittest_addNewItemWithProgressWithStatus(),
381578 "Can add a new item with progress and status.");
579+ ok(unittest_fetchItem2(),
580+ "Can fetch a singular item by ID (2).");
581+ ok(unittest_checkPre02MLFileHeaderOrder(),
582+ "Check header position variance and case-insensitivity.");
382583 }
383584
384585 private bool unittest_createNewList()
@@ -719,3 +920,105 @@ private bool unittest_addNewItemWithProgressWithStatus()
719920
720921 return true;
721922 }
923+
924+private bool unittest_fetchItem2()
925+{
926+ MediaList *list = ml_open_list(buildPath(getcwd(), "unittest_fetchItem2.tsv"));
927+
928+ if (null is list)
929+ return false;
930+
931+ scope(exit) {
932+ ml_send_command(list, MLCommand.delete_, []);
933+ ml_free_list(list);
934+ }
935+
936+ MLError ec;
937+
938+ ec = ml_send_command(list, MLCommand.add, ["Item 1"]);
939+ if (MLError.success != ec) {
940+ diag("Failed to send add command [Item 1].");
941+ return false;
942+ }
943+
944+ ec = ml_send_command(list, MLCommand.add, ["Item 2", "1/-", "READING"]);
945+ if (MLError.success != ec) {
946+ diag("Failed to send add command [Item 2, 1/-, READING]");
947+ return false;
948+ }
949+
950+ MediaListItem item2;
951+
952+ ec = ml_fetch_item(list, 2, &item2);
953+ if (MLError.success != ec) {
954+ diag(`Failed to fetch item 2 (ec = ` ~ to!string(ec) ~ `)`);
955+ return false;
956+ }
957+
958+ if ("Item 2" != item2.title) {
959+ diag(`ml_fetch_item(2).title != "Item 2". (got: ` ~ item2.title ~ ")");
960+ return false;
961+ }
962+
963+ if ("1/-" != item2.progress) {
964+ diag(`ml_fetch_item(2).progress != "1/-". (got: ` ~ item2.progress ~ ")");
965+ return false;
966+ }
967+
968+ if ("READING" != item2.status) {
969+ diag(`ml_fetch_item(2).status != "READING". (got: ` ~ item2.status ~ ")");
970+ return false;
971+ }
972+
973+ return true;
974+}
975+
976+/*
977+ * Emulate pre-0.2 medialist files to confirm that headers can be in any order
978+ * and that it is case-insensitive.
979+ */
980+private bool unittest_checkPre02MLFileHeaderOrder()
981+{
982+ enum listName = "checkPre02MLFileHeaderOrder";
983+ enum fileName = listName ~ ".tsv";
984+
985+ /* case-insensitive */
986+ auto listFile = File(fileName, "w+");
987+ listFile.writeln("TITLE\tPROGRESS\tSTATUS");
988+ listFile.writeln("Item 1\t??/??\tUNKNOWN");
989+ listFile.close();
990+
991+ MediaList *list = ml_open_list(fileName);
992+
993+ /* shouldn't crash */
994+ MediaListHeader[] headers = ml_fetch_headers(list);
995+ if (headers.length != 3) {
996+ diag("headers.length != 3 (case IS sensitive)");
997+ return false;
998+ }
999+
1000+ /* shouldn't crash */
1001+ MediaListItem[] items = ml_fetch_all(list);
1002+
1003+ ml_send_command(list, MLCommand.delete_, []);
1004+ ml_free_list(list);
1005+
1006+ listFile = File(fileName, "w+");
1007+ listFile.writeln ("PROGRESS\tTItLE\tstatus");
1008+ listFile.writeln ("??/??\tItem 1\tUNKNOWN");
1009+ listFile.close();
1010+
1011+ list = ml_open_list(fileName);
1012+ items = ml_fetch_all(list);
1013+
1014+ headers = ml_fetch_headers(list);
1015+ if (headers.length != 3) {
1016+ diag("headers.length != 3 (order IS hard-coded)");
1017+ return false;
1018+ }
1019+
1020+ ml_send_command(list, MLCommand.delete_, []);
1021+ ml_free_list(list);
1022+
1023+ return true;
1024+}
--- a/show.d
+++ /dev/null
@@ -1,227 +0,0 @@
1-/*
2- * Copyright (C) 2021 dawning.
3- *
4- * This file is part of medialist-cli.
5- *
6- * medialist-cli is free software: you can redistribute it and/or modify
7- * it under the terms of the GNU General Public License as published by
8- * the Free Software Foundation, either version 3 of the License, or
9- * (at your option) any later version.
10- *
11- * medialist-cli is distributed in the hope that it will be useful,
12- * but WITHOUT ANY WARRANTY; without even the implied warranty of
13- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14- * GNU General Public License for more details.
15- *
16- * You should have received a copy of the GNU General Public License
17- * along with medialist-cli. If not, see <https://www.gnu.org/licenses/>.
18- */
19-module show;
20-
21-import util;
22-
23-import std.array : empty, split;
24-import std.file : exists, isFile;
25-import std.path : buildPath;
26-import std.stdio : File, writef;
27-import std.typecons : Tuple;
28-
29-@trusted bool
30-handle_show(string program_name, string[] args, string data_dir)
31-{
32- bool print_numbers = false;
33- bool show_headers = false;
34- string list_name = "";
35-
36- foreach (arg; args)
37- {
38- switch (arg)
39- {
40- case "-n":
41- case "--numbered":
42- print_numbers = true;
43- break;
44- case "--show-headers":
45- show_headers = true;
46- break;
47- default:
48- if (true == list_name.empty)
49- {
50- list_name = arg;
51- }
52- }
53- }
54-
55- immutable list_file = buildPath(data_dir, list_name ~ ".tsv");
56-
57- if (false == list_file.exists || false == list_file.isFile)
58- {
59- _log("Couldn't find a list name '" ~ list_name ~ "'.", true);
60- return false;
61- }
62-
63- File f = File(list_file, "r");
64-
65- bool passed_header = false;
66- Tuple!(size_t, string)[NUMBER_OF_ML_HEADERS] headers;
67-
68- size_t index = 1;
69- size_t title_index = 0;
70- size_t status_index = 0;
71- size_t progress_index = 0;
72-
73- /* "Human Headers" as (currently not) documented */
74- /*
75- * To make up for the lack of documentation:
76- * An octothorpe (#) followed by a space ( ) is ignored as a comment.
77- * An octothorpe followed by anything else is assumed to be a key-value option.
78- *
79- * medialist-cli will use the "value" of "keys" which share a name with a header
80- * as a "Human Friendly" version. The "value" is stripped of whitespace, but not
81- * of quotation marks.
82- *
83- * Example:
84- *
85- * #progress = Progress
86- * #title=Title
87- * #progress = progress
88- * # Multiple occurrences of a "key" will result in the last one being used.
89- */
90- string title = MLHeaders.title;
91- string status = MLHeaders.status;
92- string progress = MLHeaders.progress;
93-
94- foreach (line; f.byLine)
95- {
96- if (line.empty)
97- continue;
98- if ("# " == line[0 .. 2])
99- continue;
100-
101- if ('#' == line[0] && ' ' != line[1] && false == passed_header)
102- {
103- check_human_headers (line, title, progress, status);
104- continue;
105- }
106-
107- auto sections = line.split("\t");
108- if (false == passed_header)
109- {
110- headers = parseMLHeader(cast(string[]) sections);
111- title_index = findHeaderIndex(headers, MLHeaders.title);
112- status_index = findHeaderIndex(headers, MLHeaders.status);
113- progress_index = findHeaderIndex(headers, MLHeaders.progress);
114- passed_header = true;
115- if (true == show_headers)
116- {
117- size_t i = 0;
118- writef ("%s => %s => %s\n", title, status, progress);
119- for (i = 0; i < title.length; i++)
120- writef ("=");
121- for (i = 0; i < progress.length; i++)
122- writef ("=");
123- for (i = 0; i < status.length; i++)
124- writef ("=");
125-
126- writef ("========\n");
127- }
128- }
129- else
130- {
131- if (true == print_numbers)
132- {
133- writef("%d: ", index);
134- }
135-
136- writef("%s => %s => %s\n", sections[title_index],
137- sections[status_index], sections[progress_index]);
138- index += 1;
139- }
140- }
141-
142- return true;
143-}
144-
145-private void
146-check_human_headers (in char[] line, ref string title, ref string progress, ref string status)
147-{
148- import std.algorithm.mutation : stripLeft;
149- import std.string : strip;
150-
151- string key = "";
152- string value = "";
153-
154- {
155- auto option = stripLeft(line, '#');
156- auto keyValue = split(option, "=");
157- if (2 > keyValue.length)
158- {
159- return;
160- }
161-
162- key = strip(keyValue[0]).dup;
163- value = strip(keyValue[1]).dup;
164- }
165-
166- if ("" == key)
167- return;
168-
169- switch (key)
170- {
171- case MLHeaders.title:
172- title = value;
173- break;
174- case MLHeaders.progress:
175- progress = value;
176- break;
177- case MLHeaders.status:
178- status = value;
179- break;
180- default:
181- break;
182- }
183-}
184-
185-private @safe size_t
186-findHeaderIndex(ref HeaderPairType[NUMBER_OF_ML_HEADERS] headers, string searchHeader)
187-{
188- /* reminder: HeaderPairType is Tuple!(size_t, string) */
189- foreach (header; headers)
190- {
191- if (header[1] == searchHeader)
192- {
193- return header[0];
194- }
195- }
196- return 0;
197-}
198-
199-
200-/* Emulate pre-0.2 medialist files to confirm that headers can be in any order */
201-unittest
202-{
203- import std.file : remove;
204- import std.stdio : File;
205-
206- enum listName = "unittest-show.d-pre-0.2";
207-
208- auto listFile = File(listName ~ ".tsv", "w+");
209- listFile.writeln("TITLE\tPROGRESS\tSTATUS");
210- listFile.writeln("Item 1\t??/??\tUNKNOWN");
211- listFile.close();
212-
213- /* Shouldn't crash */
214- handle_show ("medialist-cli-unittest", [listName], ".");
215-
216- remove (listName ~ ".tsv");
217-
218- listFile = File (listName ~ ".tsv", "w+");
219- listFile.writeln ("PROGRESS\tTItLE\tstatus");
220- listFile.writeln ("??/??\tItem 1\tUNKNOWN");
221- listFile.close();
222-
223- /* Also shouldn't crash */
224- handle_show ("medialist-cli-unittest", [listName], ".");
225-
226- remove (listName ~ ".tsv");
227-}
--- a/unittests.d
+++ b/unittests.d
@@ -24,7 +24,6 @@ import std.string : splitLines;
2424
2525 import medialist;
2626 import update;
27-import show;
2827
2928 import tap;
3029