77import com .cineseekerr .bot .model .ReleaseSource ;
88import com .cineseekerr .bot .model .Resolution ;
99import com .cineseekerr .bot .model .SearchResult ;
10- import com .cineseekerr .bot .model .TmdbMovie ;
10+ import com .cineseekerr .bot .model .TmdbTitle ;
1111import com .cineseekerr .bot .model .VideoCodec ;
12+ import org .springframework .stereotype .Component ;
1213
14+ import java .time .Duration ;
1315import java .util .ArrayList ;
1416import java .util .List ;
1517import java .util .Locale ;
1618import java .util .stream .Collectors ;
1719
18- /** Builds the HTML texts the bot sends. All user-controlled content goes through {@link #esc}. */
19- public final class MessageFormatter {
20+ /**
21+ * Builds the HTML texts the bot sends, in the language configured via
22+ * {@link Messages}. All user-controlled content goes through {@link #esc}.
23+ */
24+ @ Component
25+ public class MessageFormatter {
2026
2127 static final String [] NUMBER_EMOJI = {"1️⃣" , "2️⃣" , "3️⃣" , "4️⃣" , "5️⃣" };
2228
23- private MessageFormatter () {
29+ private final Messages messages ;
30+
31+ public MessageFormatter (Messages messages ) {
32+ this .messages = messages ;
2433 }
2534
2635 public static String esc (String text ) {
@@ -30,19 +39,25 @@ public static String esc(String text) {
3039 .replace (">" , ">" );
3140 }
3241
33- static String movieLabel (TmdbMovie movie ) {
34- return movie .year () == null ? movie .title () : movie .title () + " (" + movie .year () + ")" ;
42+ static String titleLabel (TmdbTitle title ) {
43+ return title .year () == null ? title .title () : title .title () + " (" + title .year () + ")" ;
44+ }
45+
46+ /** 📺 for TV series, 🎬 for movies. */
47+ static String icon (TmdbTitle title ) {
48+ return title .isTv () ? "📺" : "🎬" ;
3549 }
3650
37- static String candidatesText (List <TmdbMovie > candidates ) {
38- StringBuilder sb = new StringBuilder ("🎬 <b>Quale film intendi?</b> \n \n " );
51+ String candidatesText (List <TmdbTitle > candidates ) {
52+ StringBuilder sb = new StringBuilder (messages . get ( "candidates.header" )). append ( " \n \n " );
3953 for (int i = 0 ; i < candidates .size (); i ++) {
40- TmdbMovie movie = candidates .get (i );
41- sb .append (NUMBER_EMOJI [i ]).append (' ' ).append ("<b>" ).append (esc (movieLabel (movie ))).append ("</b>" );
42- if (movie .posterUrl () != null ) {
43- sb .append (" <a href=\" " ).append (movie .posterUrl ()).append ("\" >🖼</a>" );
54+ TmdbTitle title = candidates .get (i );
55+ sb .append (NUMBER_EMOJI [i ]).append (' ' ).append (icon (title )).append (' ' )
56+ .append ("<b>" ).append (esc (titleLabel (title ))).append ("</b>" );
57+ if (title .posterUrl () != null ) {
58+ sb .append (" <a href=\" " ).append (title .posterUrl ()).append ("\" >🖼</a>" );
4459 }
45- String overview = movie .overview ();
60+ String overview = title .overview ();
4661 if (overview != null && !overview .isBlank ()) {
4762 sb .append ('\n' ).append ("<i>" ).append (esc (truncate (overview , 120 ))).append ("</i>" );
4863 }
@@ -51,35 +66,43 @@ static String candidatesText(List<TmdbMovie> candidates) {
5166 return sb .toString ().stripTrailing ();
5267 }
5368
54- static String qualityLabel (Resolution resolution ) {
55- return resolution == Resolution .UNKNOWN ? "Altro" : resolution .label ();
69+ String qualityLabel (Resolution resolution ) {
70+ return resolution == Resolution .UNKNOWN ? messages . get ( "quality.other" ) : resolution .label ();
5671 }
5772
5873 static String subtitleLabel (Language language ) {
5974 return "SUB " + language .label ();
6075 }
6176
62- /** e.g. {@code "2160p · ITA · SUB ITA"} or {@code "nessun filtro "}. */
63- static String filterRecap (ConversationState state ) {
77+ /** e.g. {@code "ITA · SUB ITA · 2160p "} (same order as the filter steps) or {@code "no filters "}. */
78+ String filterRecap (ConversationState state ) {
6479 List <String > parts = new ArrayList <>();
65- if (state .qualityFilter () != null ) {
66- parts .add (qualityLabel (state .qualityFilter ()));
67- }
6880 if (state .audioFilter () != null ) {
6981 parts .add (state .audioFilter ().label ());
7082 }
7183 if (state .subtitleFilter () != null ) {
7284 parts .add (subtitleLabel (state .subtitleFilter ()));
7385 }
74- return parts .isEmpty () ? "nessun filtro" : String .join (" · " , parts );
86+ if (state .qualityFilter () != null ) {
87+ parts .add (qualityLabel (state .qualityFilter ()));
88+ }
89+ return parts .isEmpty () ? messages .get ("recap.none" ) : String .join (" · " , parts );
7590 }
7691
77- static String stepHeader (ConversationState state ) {
78- return "🎬 <b>" + esc (movieLabel (state .movie ())) + "</b> — "
79- + state .filtered ().size () + " release (" + filterRecap (state ) + ")\n \n " ;
92+ String stepHeader (ConversationState state ) {
93+ String label = titleLabel (state .title ());
94+ if (state .season () != null ) {
95+ label += " — " + messages .get ("recap.season" , state .season ());
96+ }
97+ if (state .episode () != null ) {
98+ label += " — " + messages .get ("recap.episode" , state .episode ());
99+ }
100+ return icon (state .title ()) + " <b>" + esc (label ) + "</b> — "
101+ + messages .get ("step.header.releases" , state .filtered ().size (), filterRecap (state ))
102+ + "\n \n " ;
80103 }
81104
82- static String releaseSummary (SearchResult result ) {
105+ String releaseSummary (SearchResult result ) {
83106 ParsedRelease parsed = result .parsed ();
84107 List <String > tech = new ArrayList <>();
85108 if (parsed .resolution () != Resolution .UNKNOWN ) {
@@ -92,7 +115,7 @@ static String releaseSummary(SearchResult result) {
92115 tech .add (parsed .codec ().label ());
93116 }
94117 String audio = parsed .audioLanguages ().isEmpty ()
95- ? " audio ?"
118+ ? messages . get ( "release. audio.unknown" )
96119 : parsed .audioLanguages ().stream ().map (Language ::label ).collect (Collectors .joining (" " ));
97120 tech .add ("🔊 " + audio );
98121 if (parsed .subtitled ()) {
@@ -106,9 +129,9 @@ static String releaseSummary(SearchResult result) {
106129 return String .join (" · " , tech );
107130 }
108131
109- static String shortlistText (ConversationState state ) {
132+ String shortlistText (ConversationState state ) {
110133 StringBuilder sb = new StringBuilder (stepHeader (state ))
111- .append ("🎯 <b>Migliori release per seeders:</b> \n \n " );
134+ .append (messages . get ( "shortlist.header" )). append ( " \n \n " );
112135 List <SearchResult > shortlist = state .shortlist ();
113136 for (int i = 0 ; i < shortlist .size (); i ++) {
114137 SearchResult result = shortlist .get (i );
@@ -122,19 +145,19 @@ static String shortlistText(ConversationState state) {
122145 .append ("<code>" ).append (esc (truncate (result .release ().title (), 80 ))).append ("</code>" )
123146 .append ("\n \n " );
124147 }
125- sb .append ("Quale scarico?" );
148+ sb .append (messages . get ( "shortlist.prompt" ) );
126149 return sb .toString ();
127150 }
128151
129- static String torrentStatusLine (QbtTorrent torrent ) {
152+ String torrentStatusLine (QbtTorrent torrent ) {
130153 String name = truncate (torrent .name (), 60 );
131154 if (torrent .isComplete ()) {
132- return "✅ <b>" + esc (name ) + "</b> — completato" ;
155+ return messages . get ( "status.line.done" , esc (name )) ;
133156 }
134157 int percent = (int ) Math .floor (torrent .progress () * 100 );
135158 long speed = torrent .dlspeed () == null ? 0 : torrent .dlspeed ();
136- return "⬇️ <b>" + esc (name ) + "</b> — " + percent + "% ("
137- + humanSize (speed ) + "/s, ETA " + humanEta (torrent .eta ()) + ")" ;
159+ return messages . get ( "status.line.progress" , esc (name ), String . valueOf ( percent ),
160+ humanSize (speed ), humanEta (torrent .eta ())) ;
138161 }
139162
140163 static String humanSize (long bytes ) {
@@ -147,6 +170,14 @@ static String humanSize(long bytes) {
147170 return String .format (Locale .ROOT , "%.0f KB" , bytes / 1024.0 );
148171 }
149172
173+ /** e.g. {@code "2h 15m"} or {@code "40m"}. */
174+ public static String humanDuration (Duration duration ) {
175+ long totalMinutes = Math .max (0 , duration .toMinutes ());
176+ long hours = totalMinutes / 60 ;
177+ long minutes = totalMinutes % 60 ;
178+ return hours > 0 ? hours + "h " + minutes + "m" : minutes + "m" ;
179+ }
180+
150181 /** qBittorrent reports 8640000 seconds when the ETA is unknown. */
151182 static String humanEta (Long etaSeconds ) {
152183 if (etaSeconds == null || etaSeconds >= 8_640_000L || etaSeconds < 0 ) {
0 commit comments