CLI interface to medialist (fossil mirror)
修订版 | d2ebf9357a59a42265e146622ddc98aae711acca (tree) |
---|---|
时间 | 2022-01-20 14:45:53 |
作者 | mio <stigma@disr...> |
Commiter | mio |
add an implementation for "update" command.
also changes the MediaListItem structure a little.
FossilOrigin-Name: 71855bed5d0d00b82f77be0aea3dacf6e66506778b4c80df5117499840f6734c
@@ -40,10 +40,9 @@ struct MediaListItem | ||
40 | 40 | string title; |
41 | 41 | string progress; |
42 | 42 | string status; |
43 | - /* start_date */ | |
44 | - /* end_date */ | |
45 | - /* last_updated */ | |
46 | - bool isValid; | |
43 | + string startDate; | |
44 | + string endDate; | |
45 | + string lastUpdated; | |
47 | 46 | } |
48 | 47 | |
49 | 48 | struct MediaListHeader |
@@ -69,6 +68,18 @@ enum MLCommand | ||
69 | 68 | * Args: ["(Optional) Item ID", ...(repeat for the amount of ids needed)] |
70 | 69 | */ |
71 | 70 | delete_, |
71 | + /** | |
72 | + * Update an item on a list. | |
73 | + * | |
74 | + * Args: ["Item ID", "field::value", "field::value", ...] | |
75 | + * | |
76 | + * For example: ml_send_command(list, MLCommand.update, ["1", "status:READING"]); | |
77 | + * The "field" is automatically converted to lowercase, while the value is kept | |
78 | + * as-is. | |
79 | + * | |
80 | + * To update the start and end date of an item, use "start_date" and | |
81 | + * "end_date" respectively. | |
82 | + */ | |
72 | 83 | update, |
73 | 84 | } |
74 | 85 |
@@ -212,6 +223,9 @@ MLError ml_fetch_item(MediaList* list, size_t id, MediaListItem* item) | ||
212 | 223 | size_t titleIndex = 0; |
213 | 224 | size_t progressIndex = 0; |
214 | 225 | size_t statusIndex = 0; |
226 | + size_t startIndex = 0; | |
227 | + size_t endIndex = 0; | |
228 | + size_t lastIndex = 0; | |
215 | 229 | |
216 | 230 | foreach(size_t idx, int[2] header; headerPositions) { |
217 | 231 | switch(header[0]) { |
@@ -224,13 +238,23 @@ MLError ml_fetch_item(MediaList* list, size_t id, MediaListItem* item) | ||
224 | 238 | case MLHeaders.status: |
225 | 239 | statusIndex = header[1]; |
226 | 240 | break; |
241 | + case MLHeaders.startDate: | |
242 | + startIndex = header[1]; | |
243 | + break; | |
244 | + case MLHeaders.endDate: | |
245 | + endIndex = header[1]; | |
246 | + break; | |
247 | + case MLHeaders.lastUpdated: | |
248 | + lastIndex = header[1]; | |
249 | + break; | |
227 | 250 | default: |
228 | 251 | break; |
229 | 252 | } |
230 | 253 | } |
231 | 254 | |
232 | 255 | MediaListItem newItem = MediaListItem(sections[titleIndex], |
233 | - sections[progressIndex], sections[statusIndex], true); | |
256 | + sections[progressIndex], sections[statusIndex], sections[startIndex], | |
257 | + sections[endIndex], sections[lastIndex]); | |
234 | 258 | |
235 | 259 | *item = newItem; |
236 | 260 |
@@ -252,12 +276,15 @@ MediaListItem[] ml_fetch_items(MediaList* list, size_t[] ids ...) | ||
252 | 276 | bool pastHeader = false; |
253 | 277 | MediaListItem[] items; |
254 | 278 | |
279 | + int[2][6] headerPositions = _ml_get_header_positions(list); | |
255 | 280 | size_t titleIndex = 0; |
256 | 281 | size_t progressIndex = 0; |
257 | 282 | size_t statusIndex = 0; |
258 | - int[2][6] headerPositions = _ml_get_header_positions(list); | |
283 | + size_t startIndex = 0; | |
284 | + size_t endIndex = 0; | |
285 | + size_t lastIndex = 0; | |
259 | 286 | |
260 | - foreach(ref header; headerPositions) { | |
287 | + foreach(size_t idx, int[2] header; headerPositions) { | |
261 | 288 | switch(header[0]) { |
262 | 289 | case MLHeaders.title: |
263 | 290 | titleIndex = header[1]; |
@@ -268,6 +295,15 @@ MediaListItem[] ml_fetch_items(MediaList* list, size_t[] ids ...) | ||
268 | 295 | case MLHeaders.status: |
269 | 296 | statusIndex = header[1]; |
270 | 297 | break; |
298 | + case MLHeaders.startDate: | |
299 | + startIndex = header[1]; | |
300 | + break; | |
301 | + case MLHeaders.endDate: | |
302 | + endIndex = header[1]; | |
303 | + break; | |
304 | + case MLHeaders.lastUpdated: | |
305 | + lastIndex = header[1]; | |
306 | + break; | |
271 | 307 | default: |
272 | 308 | break; |
273 | 309 | } |
@@ -287,8 +323,9 @@ MediaListItem[] ml_fetch_items(MediaList* list, size_t[] ids ...) | ||
287 | 323 | |
288 | 324 | if (true == canFind(ids, currentID)) { |
289 | 325 | string[] sections = line.strip().split("\t"); |
290 | - items ~= MediaListItem(sections[titleIndex], sections[progressIndex], | |
291 | - sections[statusIndex], true); | |
326 | + items ~= MediaListItem(sections[titleIndex], | |
327 | + sections[progressIndex], sections[statusIndex], sections[startIndex], | |
328 | + sections[endIndex], sections[lastIndex]); | |
292 | 329 | } |
293 | 330 | |
294 | 331 | currentID += 1; |
@@ -313,9 +350,12 @@ MediaListItem[] ml_fetch_all(MediaList* list) | ||
313 | 350 | size_t titleIndex = 0; |
314 | 351 | size_t progressIndex = 0; |
315 | 352 | size_t statusIndex = 0; |
353 | + size_t startIndex = 0; | |
354 | + size_t endIndex = 0; | |
355 | + size_t lastIndex = 0; | |
316 | 356 | |
317 | - foreach(ref header; headerPositions) { | |
318 | - switch (header[0]) { | |
357 | + foreach(size_t idx, int[2] header; headerPositions) { | |
358 | + switch(header[0]) { | |
319 | 359 | case MLHeaders.title: |
320 | 360 | titleIndex = header[1]; |
321 | 361 | break; |
@@ -325,6 +365,15 @@ MediaListItem[] ml_fetch_all(MediaList* list) | ||
325 | 365 | case MLHeaders.status: |
326 | 366 | statusIndex = header[1]; |
327 | 367 | break; |
368 | + case MLHeaders.startDate: | |
369 | + startIndex = header[1]; | |
370 | + break; | |
371 | + case MLHeaders.endDate: | |
372 | + endIndex = header[1]; | |
373 | + break; | |
374 | + case MLHeaders.lastUpdated: | |
375 | + lastIndex = header[1]; | |
376 | + break; | |
328 | 377 | default: |
329 | 378 | break; |
330 | 379 | } |
@@ -344,8 +393,9 @@ MediaListItem[] ml_fetch_all(MediaList* list) | ||
344 | 393 | |
345 | 394 | string[] sections = line.strip().split("\t"); |
346 | 395 | |
347 | - items ~= MediaListItem(sections[titleIndex], sections[progressIndex], | |
348 | - sections[statusIndex], true); | |
396 | + items ~= MediaListItem(sections[titleIndex], | |
397 | + sections[progressIndex], sections[statusIndex], sections[startIndex], | |
398 | + sections[endIndex], sections[lastIndex]); | |
349 | 399 | } |
350 | 400 | |
351 | 401 | list.isOpen = false; |
@@ -366,6 +416,7 @@ MLError ml_send_command(MediaList* list, MLCommand command, string[] args) | ||
366 | 416 | res = _ml_delete(list, args); |
367 | 417 | break; |
368 | 418 | case MLCommand.update: |
419 | + res = _ml_update(list, args); | |
369 | 420 | break; |
370 | 421 | default: |
371 | 422 | break; |
@@ -534,6 +585,182 @@ private MLError _ml_delete(MediaList* list, string[] args) | ||
534 | 585 | return MLError.success; |
535 | 586 | } |
536 | 587 | |
588 | +private MLError _ml_update(MediaList* list, string[] args) | |
589 | +{ | |
590 | + if (list.isOpen) | |
591 | + return MLError.fileAlreadyOpen; | |
592 | + | |
593 | + if (2 > args.length) | |
594 | + return MLError.invalidArgs; | |
595 | + | |
596 | + string title = null; | |
597 | + string progress = null; | |
598 | + string status = null; | |
599 | + string startDate = ""; | |
600 | + string endDate = ""; | |
601 | + | |
602 | + size_t id; | |
603 | + | |
604 | + try { | |
605 | + id = to!size_t(args[0]); | |
606 | + } catch (Exception e) { | |
607 | + return MLError.invalidArgs; | |
608 | + } | |
609 | + | |
610 | + foreach(string arg; args) { | |
611 | + string[] kv = arg.split("::"); | |
612 | + | |
613 | + if (2 > kv.length) | |
614 | + continue; | |
615 | + | |
616 | + string k = kv[0].toLower(); | |
617 | + string v = kv[1]; | |
618 | + | |
619 | + switch (k) { | |
620 | + case "title": | |
621 | + title = v; | |
622 | + break; | |
623 | + case "status": | |
624 | + status = v; | |
625 | + break; | |
626 | + case "progress": | |
627 | + progress = v; | |
628 | + break; | |
629 | + case "start_date": | |
630 | + startDate = v; | |
631 | + break; | |
632 | + case "end_date": | |
633 | + endDate = v; | |
634 | + break; | |
635 | + default: | |
636 | + break; | |
637 | + } | |
638 | + } | |
639 | + | |
640 | + string tempFilePath = buildPath(tempDir, "ml_temp.tsv"); | |
641 | + File tempFile = File(tempFilePath, "w+"); | |
642 | + scope(exit) remove(tempFilePath); | |
643 | + | |
644 | + int[2][6] headerPositions = _ml_get_header_positions(list); | |
645 | + size_t titleIndex = 0; | |
646 | + size_t progressIndex = 0; | |
647 | + size_t statusIndex = 0; | |
648 | + size_t startDateIndex = 0; | |
649 | + size_t endDateIndex = 0; | |
650 | + size_t lastUpdatedIndex = 0; | |
651 | + | |
652 | + foreach (ref header; headerPositions) { | |
653 | + switch (header[0]) { | |
654 | + case MLHeaders.title: | |
655 | + titleIndex = header[1]; | |
656 | + break; | |
657 | + case MLHeaders.progress: | |
658 | + progressIndex = header[1]; | |
659 | + break; | |
660 | + case MLHeaders.status: | |
661 | + statusIndex = header[1]; | |
662 | + break; | |
663 | + case MLHeaders.startDate: | |
664 | + startDateIndex = header[1]; | |
665 | + break; | |
666 | + case MLHeaders.endDate: | |
667 | + endDateIndex = header[1]; | |
668 | + break; | |
669 | + case MLHeaders.lastUpdated: | |
670 | + lastUpdatedIndex = header[1]; | |
671 | + break; | |
672 | + default: | |
673 | + break; | |
674 | + } | |
675 | + } | |
676 | + | |
677 | + File listFile = File(list.filePath, "r"); | |
678 | + list.isOpen = true; | |
679 | + | |
680 | + string line; | |
681 | + bool pastHeader; | |
682 | + size_t currentIndex = 1; | |
683 | + | |
684 | + while ((line = listFile.readln()) !is null) { | |
685 | + if (line.length == 0) | |
686 | + continue; | |
687 | + | |
688 | + if (line[0] == '#') { | |
689 | + tempFile.write(line); | |
690 | + continue; | |
691 | + } | |
692 | + | |
693 | + if (false == pastHeader) { | |
694 | + pastHeader = true; | |
695 | + tempFile.write(line); | |
696 | + continue; | |
697 | + } | |
698 | + | |
699 | + if (currentIndex == id) { | |
700 | + string[] sections = line.strip().split("\t"); | |
701 | + if (title !is null) | |
702 | + sections[titleIndex] = title; | |
703 | + if (progress !is null) | |
704 | + sections[progressIndex] = progress; | |
705 | + | |
706 | + if (status !is null) { | |
707 | + string oldStatus = sections[statusIndex]; | |
708 | + | |
709 | + if ((oldStatus.toLower == "plan-to-read" && status == "reading") || | |
710 | + (oldStatus.toLower == "plan-to-watch" && status == "watching")) { | |
711 | + | |
712 | + if (startDate == "") { | |
713 | + Date date = cast(Date)Clock.currTime; | |
714 | + startDate = format!"%d-%02d-%02d"(date.year, date.month, date.day); | |
715 | + sections[startDateIndex] = startDate; | |
716 | + } | |
717 | + } | |
718 | + | |
719 | + if ((oldStatus.toLower == "reading" && status == "complete") || | |
720 | + (oldStatus.toLower == "watching" && status == "complete")) { | |
721 | + | |
722 | + if (endDate == "") { | |
723 | + Date date = cast(Date)Clock.currTime; | |
724 | + endDate = format!"%d-%02d-%02d"(date.year, date.month, date.day); | |
725 | + sections[endDateIndex] = endDate; | |
726 | + } | |
727 | + } | |
728 | + | |
729 | + sections[statusIndex] = status; | |
730 | + } | |
731 | + | |
732 | + if (startDate !is null) | |
733 | + sections[startDateIndex] = startDate; | |
734 | + if (endDate !is null) | |
735 | + sections[endDateIndex] = endDate; | |
736 | + | |
737 | + Date date = cast(Date)Clock.currTime(); | |
738 | + sections[lastUpdatedIndex] = format!"%d-%02d-%02d"(date.year, date.month, date.day); | |
739 | + tempFile.writeln(join(sections, "\t")); | |
740 | + } else { | |
741 | + tempFile.write(line); | |
742 | + } | |
743 | + | |
744 | + currentIndex += 1; | |
745 | + } | |
746 | + | |
747 | + listFile.close(); | |
748 | + tempFile.flush(); | |
749 | + tempFile.close(); | |
750 | + | |
751 | + tempFile = File(tempFilePath, "r"); | |
752 | + listFile = File(list.filePath, "w+"); | |
753 | + | |
754 | + while ((line = tempFile.readln()) !is null) { | |
755 | + listFile.write(line); | |
756 | + } | |
757 | + listFile.flush(); | |
758 | + | |
759 | + list.isOpen = false; | |
760 | + | |
761 | + return MLError.success; | |
762 | +} | |
763 | + | |
537 | 764 | private enum MLHeaders |
538 | 765 | { |
539 | 766 | title = 0, |
@@ -639,6 +866,14 @@ public void runMediaListUnitTests() | ||
639 | 866 | "Check header position variance and case-insensitivity."); |
640 | 867 | ok(unittest_fetchItems124(), |
641 | 868 | "Can fetch multiple items by ID (1, 4, and 2)."); |
869 | + ok(unittest_updateItemTitle(), | |
870 | + "Can update an item's title."); | |
871 | + ok(unittest_updateItemStatus(), | |
872 | + "Can update an item's status."); | |
873 | + ok(unittest_updateItemProgress(), | |
874 | + "Can update an item's progress."); | |
875 | + ok(unittest_updateItemAll(), | |
876 | + "Can update all aspects of an item."); | |
642 | 877 | } |
643 | 878 | |
644 | 879 | private bool unittest_createNewList() |
@@ -1152,3 +1387,217 @@ private bool unittest_fetchItems124() | ||
1152 | 1387 | |
1153 | 1388 | return true; |
1154 | 1389 | } |
1390 | + | |
1391 | +private bool unittest_updateItemTitle() | |
1392 | +{ | |
1393 | + enum listName = __FUNCTION__; | |
1394 | + enum fileName = listName ~ ".tsv"; | |
1395 | + | |
1396 | + MediaList* list = ml_open_list(fileName); | |
1397 | + if (list is null) { | |
1398 | + diag("ml_open_list returned null."); | |
1399 | + return false; | |
1400 | + } | |
1401 | + | |
1402 | + scope(exit) { | |
1403 | + ml_send_command(list, MLCommand.delete_, []); | |
1404 | + ml_free_list(list); | |
1405 | + } | |
1406 | + | |
1407 | + MLError res; | |
1408 | + | |
1409 | + res = ml_send_command(list, MLCommand.add, ["Ietm 1"]); | |
1410 | + if (MLError.success != res) { | |
1411 | + diag("ml_send_command(list, add, [Ietm 1]) failed."); | |
1412 | + return false; | |
1413 | + } | |
1414 | + | |
1415 | + res = ml_send_command(list, MLCommand.update, ["1", "title::Item 1"]); | |
1416 | + if (MLError.success != res) { | |
1417 | + diag("Failed to send UPDATE command [1, title::Item 1]."); | |
1418 | + return false; | |
1419 | + } | |
1420 | + | |
1421 | + MediaListItem item; | |
1422 | + res = ml_fetch_item(list, 1, &item); | |
1423 | + if (MLError.success != res) { | |
1424 | + diag("Failed to fetch item 1 from list."); | |
1425 | + return false; | |
1426 | + } | |
1427 | + | |
1428 | + if ("Item 1" != item.title) { | |
1429 | + diag("Failed to update title from 'Ietm 1' to 'Item 1'."); | |
1430 | + return false; | |
1431 | + } | |
1432 | + | |
1433 | + return true; | |
1434 | +} | |
1435 | + | |
1436 | +private bool unittest_updateItemStatus() | |
1437 | +{ | |
1438 | + enum listName = __FUNCTION__; | |
1439 | + enum fileName = listName ~ ".tsv"; | |
1440 | + | |
1441 | + MediaList* list = ml_open_list(fileName); | |
1442 | + if (list is null) { | |
1443 | + diag("ml_open_list returned null."); | |
1444 | + return false; | |
1445 | + } | |
1446 | + | |
1447 | + scope(exit) { | |
1448 | + ml_send_command(list, MLCommand.delete_, []); | |
1449 | + ml_free_list(list); | |
1450 | + } | |
1451 | + | |
1452 | + MLError res; | |
1453 | + | |
1454 | + res = ml_send_command(list, MLCommand.add, ["Item 1", null, "PLAN-TO-READ"]); | |
1455 | + if (MLError.success != res) { | |
1456 | + diag("ml_send_command(list, add, [Item 1, null, PLAN-TO-READ]) failed."); | |
1457 | + return false; | |
1458 | + } | |
1459 | + | |
1460 | + res = ml_send_command(list, MLCommand.update, ["1", "status::READING"]); | |
1461 | + if (MLError.success != res) { | |
1462 | + diag("Failed to send UPDATE command [1, status::READING]."); | |
1463 | + return false; | |
1464 | + } | |
1465 | + | |
1466 | + MediaListItem item; | |
1467 | + res = ml_fetch_item(list, 1, &item); | |
1468 | + if (MLError.success != res) { | |
1469 | + diag("Failed to fetch item 1 from list."); | |
1470 | + return false; | |
1471 | + } | |
1472 | + | |
1473 | + if ("READING" != item.status) { | |
1474 | + diag("Failed to update status from 'PLAN-TO-READ' to 'READING'."); | |
1475 | + return false; | |
1476 | + } | |
1477 | + | |
1478 | + return true; | |
1479 | +} | |
1480 | + | |
1481 | +private bool unittest_updateItemProgress() | |
1482 | +{ | |
1483 | + enum listName = __FUNCTION__; | |
1484 | + enum fileName = listName ~ ".tsv"; | |
1485 | + | |
1486 | + MediaList* list = ml_open_list(fileName); | |
1487 | + if (list is null) { | |
1488 | + diag("ml_open_list returned null."); | |
1489 | + return false; | |
1490 | + } | |
1491 | + | |
1492 | + scope(exit) { | |
1493 | + ml_send_command(list, MLCommand.delete_, []); | |
1494 | + ml_free_list(list); | |
1495 | + } | |
1496 | + | |
1497 | + MLError res; | |
1498 | + | |
1499 | + res = ml_send_command(list, MLCommand.add, ["Item 1"]); | |
1500 | + if (MLError.success != res) { | |
1501 | + diag("ml_send_command(list, add, [Item 1]) failed."); | |
1502 | + return false; | |
1503 | + } | |
1504 | + | |
1505 | + res = ml_send_command(list, MLCommand.update, ["1", "progress::10/10"]); | |
1506 | + if (MLError.success != res) { | |
1507 | + diag("Failed to send UPDATE command [1, progress::10/10]."); | |
1508 | + return false; | |
1509 | + } | |
1510 | + | |
1511 | + MediaListItem item; | |
1512 | + res = ml_fetch_item(list, 1, &item); | |
1513 | + if (MLError.success != res) { | |
1514 | + diag("Failed to fetch item 1 from list."); | |
1515 | + return false; | |
1516 | + } | |
1517 | + | |
1518 | + if ("10/10" != item.progress) { | |
1519 | + diag("Failed to update progress from '-/-' to '10/10'."); | |
1520 | + return false; | |
1521 | + } | |
1522 | + | |
1523 | + return true; | |
1524 | +} | |
1525 | + | |
1526 | +private bool unittest_updateItemAll() | |
1527 | +{ | |
1528 | + enum listName = __FUNCTION__; | |
1529 | + enum fileName = listName ~ ".tsv"; | |
1530 | + | |
1531 | + MediaList* list = ml_open_list(fileName); | |
1532 | + if (list is null) { | |
1533 | + diag("ml_open_list returned null."); | |
1534 | + return false; | |
1535 | + } | |
1536 | + | |
1537 | + scope(exit) { | |
1538 | + ml_send_command(list, MLCommand.delete_, []); | |
1539 | + ml_free_list(list); | |
1540 | + } | |
1541 | + | |
1542 | + MLError res; | |
1543 | + | |
1544 | + res = ml_send_command(list, MLCommand.add, ["Ietm 1"]); | |
1545 | + if (MLError.success != res) { | |
1546 | + diag("ml_send_command(list, add, [Ietm 1]) failed."); | |
1547 | + return false; | |
1548 | + } | |
1549 | + | |
1550 | + res = ml_send_command(list, MLCommand.update, | |
1551 | + ["1", | |
1552 | + "start_date::2021-02-16", | |
1553 | + "end_date::2022-01-20", | |
1554 | + "progress::60/100", | |
1555 | + "status::READING", | |
1556 | + "title::MediaList"] | |
1557 | + ); | |
1558 | + if (MLError.success != res) { | |
1559 | + diag("Failed to send UPDATE command."); | |
1560 | + return false; | |
1561 | + } | |
1562 | + | |
1563 | + MediaListItem item; | |
1564 | + res = ml_fetch_item(list, 1, &item); | |
1565 | + if (MLError.success != res) { | |
1566 | + diag("Failed to fetch item 1 from list."); | |
1567 | + return false; | |
1568 | + } | |
1569 | + | |
1570 | + if ("MediaList" != item.title) { | |
1571 | + diag("Failed to update item title from 'Ietm 1' to 'MediaList'."); | |
1572 | + return false; | |
1573 | + } | |
1574 | + | |
1575 | + if ("60/100" != item.progress) { | |
1576 | + diag("Failed to update progress from '-/-' to '60/100'."); | |
1577 | + return false; | |
1578 | + } | |
1579 | + | |
1580 | + if ("READING" != item.status) { | |
1581 | + diag("Failed to update status from 'UNKNOWN' to 'READING'."); | |
1582 | + return false; | |
1583 | + } | |
1584 | + | |
1585 | + if ("2021-02-16" != item.startDate) { | |
1586 | + diag("Failed to update start_date from '' to '2021-02-16'."); | |
1587 | + return false; | |
1588 | + } | |
1589 | + | |
1590 | + if ("2022-01-20" != item.endDate) { | |
1591 | + diag("Failed to update end_date from '' to '2022-01-20'."); | |
1592 | + return false; | |
1593 | + } | |
1594 | + | |
1595 | + Date date = cast(Date)Clock.currTime; | |
1596 | + string currentDate = format!"%d-%02d-%02d"(date.year, date.month, date.day); | |
1597 | + if (currentDate != item.lastUpdated) { | |
1598 | + diag("Failed to update last_update when running the UPDATE command."); | |
1599 | + return false; | |
1600 | + } | |
1601 | + | |
1602 | + return true; | |
1603 | +} |