Coverage for seedboxsync/cli/commands/cmd_search.py: 100%

38 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-26 17:14 +0000

1# 

2# Copyright (C) 2015-2026 Guillaume Kulakowski <guillaume@kulakowski.fr> 

3# 

4# For the full copyright and license information, please view the LICENSE 

5# file that was distributed with this source code. 

6# 

7"""All commands related to search operations in SeedboxSync.""" 

8 

9import click 

10from peewee import fn 

11from seedboxsync.cli import Context, group, pass_context 

12from seedboxsync.core.database.models import Download, Torrent 

13 

14 

15@group("search", help="Search operations.") # type: ignore[untyped-decorator] 

16@pass_context 

17def cli(ctx: Context) -> None: 

18 """Empty function for Click sub commands.""" 

19 

20 

21@cli.command("uploaded", help="Search last torrents uploaded from blackhole.") # type: ignore[untyped-decorator] 

22@click.option("-n", "--number", type=int, default=10, help="Number of torrents to display.") 

23@click.option("-s", "--search", help="Term to search.") 

24@pass_context 

25def uploaded(ctx: Context, number: int, search: str) -> None: 

26 """ 

27 Search for the most recent torrents uploaded from blackhole. 

28 

29 Filters torrents by an optional search term and limits 

30 the number of results displayed. 

31 

32 Renders a list of torrent IDs, names, and sent timestamps. 

33 

34 Args: 

35 ctx (Context): The Click context object. 

36 number (int): The maximum number of torrents to display. 

37 search (str): An optional search term to filter torrent names. 

38 """ 

39 # Build "where" expression 

40 conditions = [] 

41 if search: 

42 conditions.append(Torrent.name.contains(search)) 

43 

44 # DB query 

45 query = ( 

46 Torrent.select( 

47 Torrent.id, 

48 Torrent.name, 

49 fn.coalesce(fn.humanize(Torrent.total_size), "").alias("total_size"), 

50 fn.coalesce(Torrent.total_files, "").alias("total_files"), 

51 fn.short_datetime(Torrent.sent), 

52 ) 

53 .limit(number) 

54 .order_by(Torrent.sent.desc()) 

55 ) 

56 

57 # if "where" expression 

58 if conditions: 

59 query = query.where(*conditions) 

60 data = query.dicts() 

61 

62 click.echo( 

63 ctx.render( 

64 reversed(data), 

65 headers={"id": "Id", "name": "Name", "total_files": "File(s)", "total_size": "Size", "sent": "Sent datetime"}, 

66 ) 

67 ) 

68 

69 

70@cli.command("downloaded", help="Search last files downloaded from seedbox.") # type: ignore[untyped-decorator] 

71@click.option("-n", "--number", type=int, default=10, help="Number of torrents to display.") 

72@click.option("-s", "--search", help="Term to search.") 

73@pass_context 

74def downloaded(ctx: Context, number: int, search: str) -> None: 

75 """ 

76 Search for the most recent files downloaded from the seedbox. 

77 

78 Filters downloads by an optional search term and limits 

79 the number of results displayed. 

80 

81 Renders a list of download IDs, paths, finished timestamps, and sizes. 

82 

83 Args: 

84 ctx (Context): The Click context object. 

85 number (int): The maximum number of torrents to display. 

86 search (str): An optional search term to filter torrent names. 

87 """ 

88 # Build "where" expression 

89 where = (Download.finished != 0) & Download.path.contains(search) if search else Download.finished != 0 

90 

91 # DB query 

92 data = ( 

93 Download.select( 

94 Download.id, 

95 fn.SUBSTR(Download.path, -100).alias("path"), 

96 fn.short_datetime(Download.finished), 

97 fn.humanize(Download.local_size).alias("size"), 

98 ) 

99 .where(where) 

100 .limit(number) 

101 .order_by(Download.finished.desc()) 

102 .dicts() 

103 ) 

104 

105 click.echo( 

106 ctx.render( 

107 reversed(data), 

108 headers={ 

109 "id": "Id", 

110 "path": "Path", 

111 "finished": "Finished", 

112 "size": "Size", 

113 }, 

114 ) 

115 ) 

116 

117 

118@cli.command("progress", help="Search files currently in download from seedbox.") # type: ignore[untyped-decorator] 

119@click.option("-n", "--number", type=int, default=10, help="Number of torrents to display.") 

120@click.option("-s", "--search", help="Term to search.") 

121@pass_context 

122def progress(ctx: Context, number: int, search: str) -> None: 

123 """ 

124 Search for files currently in download from the seedbox. 

125 

126 Filters in-progress downloads by an optional search term and limits 

127 the number of results displayed. 

128 

129 Calculates local download progress and ETA, and renders a list 

130 including ID, path, start time, progress percentage, ETA, and size. 

131 

132 Args: 

133 ctx (Context): The Click context object. 

134 number (int): The maximum number of torrents to display. 

135 search (str): An optional search term to filter torrent names. 

136 """ 

137 # Build "where" expression 

138 where = (Download.finished == 0) & Download.path.contains(search) if search else Download.finished == 0 

139 

140 # Calculate columns 

141 progress_expr = 100.0 * Download.local_size / fn.NULLIF(Download.seedbox_size, 0) 

142 eta_expr = (fn.STRFTIME("%s", "now", "localtime") - fn.STRFTIME("%s", Download.started)) * (100.0 - progress_expr) / fn.NULLIF(progress_expr, 0) 

143 

144 # DB query 

145 data = ( 

146 Download.select( 

147 Download.id, 

148 fn.SUBSTR(Download.path, -100).alias("path"), 

149 fn.short_datetime(Download.started), 

150 fn.ROUND(progress_expr, 0).cast("INTEGER").concat("%").alias("progress"), 

151 fn.naturaldelta(eta_expr).alias("eta"), 

152 fn.humanize(Download.seedbox_size).alias("size"), 

153 ) 

154 .where(where) 

155 .limit(number) 

156 .order_by(Download.started.desc()) 

157 .dicts() 

158 ) 

159 

160 click.echo( 

161 ctx.render( 

162 reversed(data), 

163 headers={"id": "Id", "path": "Path", "started": "Started", "progress": "Progress", "eta": "ETA", "size": "Size"}, 

164 ) 

165 )