root | cd43656 | 2025-05-08 14:09:19 +0000 | [diff] [blame] | 1 | package tracker; |
root | 0d8b11f | 2025-05-15 14:10:43 +0000 | [diff] [blame] | 2 | import java.io.File; |
root | d4959a8 | 2025-05-27 07:07:37 +0000 | [diff] [blame] | 3 | import java.nio.file.Files; |
| 4 | import java.nio.file.Path; |
| 5 | import java.nio.file.Paths; |
| 6 | import java.nio.file.StandardCopyOption; |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 7 | import java.security.MessageDigest; |
root | d4959a8 | 2025-05-27 07:07:37 +0000 | [diff] [blame] | 8 | import java.time.LocalDateTime; |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 9 | import java.util.HashMap; |
| 10 | import java.util.List; |
| 11 | import java.util.Map; |
| 12 | import java.util.UUID; |
| 13 | import javax.persistence.*; |
| 14 | import com.dampcake.bencode.Bencode; |
| 15 | import com.dampcake.bencode.Type; |
| 16 | import com.querydsl.jpa.impl.JPAUpdateClause; |
| 17 | import entity.*; |
| 18 | import entity.config; |
TRM-coding | 508b31f | 2025-06-09 02:07:14 +0800 | [diff] [blame] | 19 | import java.util.Scanner; |
| 20 | import java.io.IOException; |
root | 0d8b11f | 2025-05-15 14:10:43 +0000 | [diff] [blame] | 21 | public class Tracker implements TrackerInterface { |
root | ff0769a | 2025-05-18 17:24:41 +0000 | [diff] [blame] | 22 | private final EntityManagerFactory emf; |
root | ff0769a | 2025-05-18 17:24:41 +0000 | [diff] [blame] | 23 | // 默认构造:产线数据库 |
| 24 | public Tracker() { |
| 25 | config cfg = new config(); |
| 26 | Map<String,Object> props = new HashMap<>(); |
| 27 | props.put("javax.persistence.jdbc.url", |
| 28 | "jdbc:mysql://" + cfg.SqlURL + "/" + cfg.Database); |
| 29 | props.put("javax.persistence.jdbc.user", cfg.SqlUsername); |
| 30 | props.put("javax.persistence.jdbc.password", cfg.SqlPassword); |
| 31 | this.emf = Persistence.createEntityManagerFactory("myPersistenceUnit", props); |
| 32 | } |
root | ff0769a | 2025-05-18 17:24:41 +0000 | [diff] [blame] | 33 | // 测试传入:测试库 |
| 34 | public Tracker(EntityManagerFactory emf) { |
| 35 | this.emf = emf; |
| 36 | } |
root | 0d8b11f | 2025-05-15 14:10:43 +0000 | [diff] [blame] | 37 | @Override |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 38 | public boolean AddUpLoad(String userid, int upload, String infoHash) { |
| 39 | long newTotal = upload; // convert to long |
root | 0d8b11f | 2025-05-15 14:10:43 +0000 | [diff] [blame] | 40 | EntityManager em = emf.createEntityManager(); |
| 41 | EntityTransaction tx = em.getTransaction(); |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 42 | tx.begin(); |
root | 0d8b11f | 2025-05-15 14:10:43 +0000 | [diff] [blame] | 43 | try { |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 44 | // 1) find the seedId by infoHash |
TRM-coding | 83df9e2 | 2025-06-09 17:51:05 +0800 | [diff] [blame] | 45 | System.out.println("DEBUG AddUpLoad: Looking for infoHash: '" + infoHash + "' (length: " + infoHash.length() + ")"); |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 46 | String seedId = em.createQuery( |
| 47 | "SELECT s.seedId FROM SeedHash s WHERE s.infoHash = :ih", String.class) |
| 48 | .setParameter("ih", infoHash) |
| 49 | .getSingleResult(); |
| 50 | // 2) sum existing uploads for this user+seed |
| 51 | Long sumSoFar = em.createQuery( |
| 52 | "SELECT COALESCE(SUM(t.upload),0) FROM TransRecord t WHERE t.uploaduserid = :uid AND t.seedid = :sid", |
| 53 | Long.class) |
| 54 | .setParameter("uid", userid) |
| 55 | .setParameter("sid", seedId) |
| 56 | .getSingleResult(); |
| 57 | long delta = newTotal - sumSoFar; |
| 58 | if (delta < 0L) { |
| 59 | tx.rollback(); |
TRM-coding | 508b31f | 2025-06-09 02:07:14 +0800 | [diff] [blame] | 60 | return false; // error: newTotal less than already recorded |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 61 | } |
| 62 | if (delta == 0L) { |
| 63 | tx.rollback(); |
| 64 | return false; // nothing to do |
| 65 | } |
| 66 | // 3) persist a new TransRecord with only the delta |
| 67 | TransRecord rd = new TransRecord(); |
| 68 | rd.taskid = UUID.randomUUID().toString(); |
| 69 | rd.uploaduserid = userid; |
| 70 | rd.seedid = seedId; |
| 71 | rd.upload = delta; |
| 72 | rd.maxupload = newTotal; |
| 73 | em.persist(rd); |
TRM-coding | 508b31f | 2025-06-09 02:07:14 +0800 | [diff] [blame] | 74 | em.flush(); |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 75 | // 4) 重新计算用户的总上传,确保与 TransRecord 完全一致 |
| 76 | Long totalUpload = em.createQuery( |
| 77 | "SELECT COALESCE(SUM(t.upload),0) FROM TransRecord t WHERE t.uploaduserid = :uid", |
| 78 | Long.class |
| 79 | ) |
| 80 | .setParameter("uid", userid) |
| 81 | .getSingleResult(); |
TRM-coding | 508b31f | 2025-06-09 02:07:14 +0800 | [diff] [blame] | 82 | |
| 83 | Long PTuploadbefor = em.createQuery( |
| 84 | "SELECT t.upload FROM UserPT t WHERE t.userid = :uid", |
| 85 | Long.class |
| 86 | ).setParameter("uid", userid) |
| 87 | .getSingleResult(); |
| 88 | |
| 89 | |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 90 | UserPT user = em.find(UserPT.class, userid); |
| 91 | user.upload = totalUpload; |
| 92 | em.merge(user); |
TRM-coding | 508b31f | 2025-06-09 02:07:14 +0800 | [diff] [blame] | 93 | em.flush(); |
root | 0d8b11f | 2025-05-15 14:10:43 +0000 | [diff] [blame] | 94 | tx.commit(); |
TRM-coding | 508b31f | 2025-06-09 02:07:14 +0800 | [diff] [blame] | 95 | |
| 96 | Long PTuploadafter = em.createQuery( |
| 97 | "SELECT t.upload FROM UserPT t WHERE t.userid = :uid", |
| 98 | Long.class |
| 99 | ).setParameter("uid", userid) |
| 100 | .getSingleResult(); |
| 101 | |
TRM-coding | 83df9e2 | 2025-06-09 17:51:05 +0800 | [diff] [blame] | 102 | System.out.println("------------------------------------------------"); |
TRM-coding | 508b31f | 2025-06-09 02:07:14 +0800 | [diff] [blame] | 103 | System.out.printf("thisadd:%d userptsofar:%d userptafter:%d totaluploadnow:%d delta:%d%n",upload, PTuploadbefor,PTuploadafter, totalUpload,delta); |
TRM-coding | 83df9e2 | 2025-06-09 17:51:05 +0800 | [diff] [blame] | 104 | System.out.println("------------------------------------------------"); |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 105 | return false; // success |
| 106 | } catch (RuntimeException ex) { |
root | 0d8b11f | 2025-05-15 14:10:43 +0000 | [diff] [blame] | 107 | if (tx.isActive()) tx.rollback(); |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 108 | throw ex; |
root | 0d8b11f | 2025-05-15 14:10:43 +0000 | [diff] [blame] | 109 | } finally { |
| 110 | em.close(); |
| 111 | } |
| 112 | } |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 113 | |
root | 0d8b11f | 2025-05-15 14:10:43 +0000 | [diff] [blame] | 114 | @Override |
root | ff0769a | 2025-05-18 17:24:41 +0000 | [diff] [blame] | 115 | public boolean ReduceUpLoad(String userid, int upload){ |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 116 | long uploadLong = upload; // convert to long |
root | ff0769a | 2025-05-18 17:24:41 +0000 | [diff] [blame] | 117 | EntityManager em = emf.createEntityManager(); |
| 118 | EntityTransaction tx = em.getTransaction(); |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 119 | tx.begin(); |
root | ff0769a | 2025-05-18 17:24:41 +0000 | [diff] [blame] | 120 | try { |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 121 | // 1) fetch user and ensure enough upload to reduce |
| 122 | UserPT user = em.find(UserPT.class, userid); |
| 123 | long before = user.upload; |
| 124 | if (uploadLong > before) { |
| 125 | tx.rollback(); |
| 126 | return true; // error: cannot reduce more than current total |
| 127 | } |
| 128 | // 2) subtract |
| 129 | user.upload = before - uploadLong; |
| 130 | em.merge(user); |
| 131 | // (optional) record a negative TransRecord so sums stay in sync |
| 132 | TransRecord rd = new TransRecord(); |
| 133 | rd.taskid = UUID.randomUUID().toString(); |
| 134 | rd.uploaduserid = userid; |
| 135 | rd.seedid = null; |
| 136 | rd.upload = -uploadLong; |
| 137 | rd.maxupload = user.upload; |
| 138 | em.persist(rd); |
root | ff0769a | 2025-05-18 17:24:41 +0000 | [diff] [blame] | 139 | tx.commit(); |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 140 | return false; // success |
| 141 | } catch (RuntimeException ex) { |
root | ff0769a | 2025-05-18 17:24:41 +0000 | [diff] [blame] | 142 | if (tx.isActive()) tx.rollback(); |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 143 | throw ex; |
root | ff0769a | 2025-05-18 17:24:41 +0000 | [diff] [blame] | 144 | } finally { |
| 145 | em.close(); |
| 146 | } |
| 147 | } |
root | 0d8b11f | 2025-05-15 14:10:43 +0000 | [diff] [blame] | 148 | @Override |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 149 | public boolean AddDownload(String userid, int download, String infoHash) { |
| 150 | long newTotal = download; // convert to long |
root | f35409f | 2025-05-19 04:41:57 +0000 | [diff] [blame] | 151 | EntityManager em = emf.createEntityManager(); |
| 152 | EntityTransaction tx = em.getTransaction(); |
| 153 | try { |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 154 | // 1. 查 SeedHash |
TRM-coding | 83df9e2 | 2025-06-09 17:51:05 +0800 | [diff] [blame] | 155 | System.out.println("DEBUG AddDownload: Looking for infoHash: '" + infoHash + "' (length: " + infoHash.length() + ")"); |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 156 | TypedQuery<SeedHash> qsh = em.createQuery( |
| 157 | "SELECT s FROM SeedHash s WHERE s.infoHash = :h", SeedHash.class); |
| 158 | qsh.setParameter("h", infoHash); |
| 159 | List<SeedHash> shl = qsh.getResultList(); |
| 160 | if (shl.isEmpty()) { |
| 161 | System.out.println("seed没有被记录"); |
TRM-coding | 508b31f | 2025-06-09 02:07:14 +0800 | [diff] [blame] | 162 | return false; |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 163 | } |
| 164 | String seedid = shl.get(0).seedId; |
| 165 | |
| 166 | // 2. 统计该用户在该种子上的已有 download |
| 167 | TypedQuery<Long> qsum = em.createQuery( |
| 168 | "SELECT COALESCE(SUM(t.download),0) FROM TransRecord t " + |
| 169 | "WHERE t.seedid = :sid AND t.downloaduserid = :uid", Long.class); |
| 170 | qsum.setParameter("sid", seedid); |
| 171 | qsum.setParameter("uid", userid); |
| 172 | long oldSeedSum = qsum.getSingleResult(); |
| 173 | |
| 174 | long diff = newTotal - oldSeedSum; |
| 175 | if (diff <= 0) return false; |
| 176 | |
| 177 | System.out.println("AddDownload: 该种子原有总量=" + oldSeedSum + ", 新总量=" + newTotal + ", 增量=" + diff); |
| 178 | |
| 179 | try { |
| 180 | tx.begin(); |
| 181 | // 1. persist 增量记录 |
| 182 | TransRecord tr = new TransRecord(); |
| 183 | tr.taskid = UUID.randomUUID().toString(); |
| 184 | tr.downloaduserid = userid; |
| 185 | tr.seedid = seedid; |
| 186 | tr.download = diff; |
| 187 | tr.maxdownload = newTotal; |
| 188 | em.persist(tr); |
| 189 | |
| 190 | // 2. 全表重新累计该用户所有种子的 download,并更新 UserPT.download |
| 191 | TypedQuery<Long> qTotal = em.createQuery( |
| 192 | "SELECT COALESCE(SUM(t.download),0) FROM TransRecord t WHERE t.downloaduserid = :uid", |
| 193 | Long.class |
| 194 | ) |
| 195 | .setParameter("uid", userid); |
| 196 | long userTotalDownload = qTotal.getSingleResult(); |
| 197 | QUserPT quser = QUserPT.userPT; |
| 198 | new JPAUpdateClause(em, quser) |
| 199 | .where(quser.userid.eq(userid)) |
| 200 | .set(quser.download, userTotalDownload) |
| 201 | .execute(); |
| 202 | |
| 203 | tx.commit(); |
| 204 | return false; |
| 205 | } catch (Exception e) { |
| 206 | if (tx.isActive()) tx.rollback(); |
| 207 | return true; |
| 208 | } finally { |
| 209 | em.close(); |
| 210 | } |
| 211 | } catch (Exception e) { |
root | f35409f | 2025-05-19 04:41:57 +0000 | [diff] [blame] | 212 | return true; |
root | f35409f | 2025-05-19 04:41:57 +0000 | [diff] [blame] | 213 | } |
| 214 | } |
root | 0d8b11f | 2025-05-15 14:10:43 +0000 | [diff] [blame] | 215 | @Override |
root | f35409f | 2025-05-19 04:41:57 +0000 | [diff] [blame] | 216 | public boolean ReduceDownload(String userid, int download) { |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 217 | long downloadLong = download; // convert to long |
root | f35409f | 2025-05-19 04:41:57 +0000 | [diff] [blame] | 218 | EntityManager em = emf.createEntityManager(); |
root | f35409f | 2025-05-19 04:41:57 +0000 | [diff] [blame] | 219 | try { |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 220 | // 1. 预检查当前值 |
| 221 | TypedQuery<Long> qcurr = em.createQuery( |
| 222 | "SELECT u.download FROM UserPT u WHERE u.userid = :uid", Long.class); |
| 223 | qcurr.setParameter("uid", userid); |
| 224 | long current = qcurr.getSingleResult(); |
| 225 | if (downloadLong > current) { |
| 226 | em.close(); |
TRM-coding | 508b31f | 2025-06-09 02:07:14 +0800 | [diff] [blame] | 227 | return false; |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 228 | } |
| 229 | // 2. 执行减法更新 |
| 230 | EntityTransaction tx = em.getTransaction(); |
root | f35409f | 2025-05-19 04:41:57 +0000 | [diff] [blame] | 231 | tx.begin(); |
| 232 | QUserPT q = QUserPT.userPT; |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 233 | new JPAUpdateClause(em, q) |
root | f35409f | 2025-05-19 04:41:57 +0000 | [diff] [blame] | 234 | .where(q.userid.eq(userid)) |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 235 | .set(q.download, q.download.subtract(downloadLong)) |
root | f35409f | 2025-05-19 04:41:57 +0000 | [diff] [blame] | 236 | .execute(); |
| 237 | tx.commit(); |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 238 | return false; |
root | f35409f | 2025-05-19 04:41:57 +0000 | [diff] [blame] | 239 | } catch(Exception e) { |
root | f35409f | 2025-05-19 04:41:57 +0000 | [diff] [blame] | 240 | return true; |
| 241 | } finally { |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 242 | if (em.isOpen()) em.close(); |
root | f35409f | 2025-05-19 04:41:57 +0000 | [diff] [blame] | 243 | } |
| 244 | } |
root | 0d8b11f | 2025-05-15 14:10:43 +0000 | [diff] [blame] | 245 | @Override |
root | f35409f | 2025-05-19 04:41:57 +0000 | [diff] [blame] | 246 | public boolean AddMagic(String userid, int magic) { |
| 247 | EntityManager em = emf.createEntityManager(); |
| 248 | EntityTransaction tx = em.getTransaction(); |
| 249 | try { |
| 250 | tx.begin(); |
| 251 | QUserPT q = QUserPT.userPT; |
| 252 | long updated = new JPAUpdateClause(em, q) |
| 253 | .where(q.userid.eq(userid)) |
| 254 | .set(q.magic, q.magic.add(magic)) |
| 255 | .execute(); |
| 256 | tx.commit(); |
| 257 | return updated <= 0; |
| 258 | } catch(Exception e) { |
| 259 | if (tx.isActive()) tx.rollback(); |
| 260 | return true; |
| 261 | } finally { |
| 262 | em.close(); |
| 263 | } |
| 264 | } |
root | 0d8b11f | 2025-05-15 14:10:43 +0000 | [diff] [blame] | 265 | @Override |
root | f35409f | 2025-05-19 04:41:57 +0000 | [diff] [blame] | 266 | public boolean ReduceMagic(String userid, int magic) { |
| 267 | EntityManager em = emf.createEntityManager(); |
| 268 | EntityTransaction tx = em.getTransaction(); |
| 269 | try { |
| 270 | tx.begin(); |
| 271 | QUserPT q = QUserPT.userPT; |
| 272 | long updated = new JPAUpdateClause(em, q) |
| 273 | .where(q.userid.eq(userid)) |
| 274 | .set(q.magic, q.magic.subtract(magic)) |
| 275 | .execute(); |
| 276 | tx.commit(); |
| 277 | return updated <= 0; |
| 278 | } catch(Exception e) { |
| 279 | if (tx.isActive()) tx.rollback(); |
| 280 | return true; |
| 281 | } finally { |
| 282 | em.close(); |
| 283 | } |
| 284 | } |
root | d4959a8 | 2025-05-27 07:07:37 +0000 | [diff] [blame] | 285 | @Override |
| 286 | public int SaveTorrent(String seedid, File TTorent){ |
| 287 | try { |
| 288 | Path storageDir = Paths.get(config.TORRENT_STORAGE_DIR); |
| 289 | if (!Files.exists(storageDir)) { |
| 290 | Files.createDirectories(storageDir); |
| 291 | } |
| 292 | String filename = TTorent.getName(); |
| 293 | Path target = storageDir.resolve(seedid + "_" + filename); |
| 294 | Files.copy(TTorent.toPath(), target, StandardCopyOption.REPLACE_EXISTING); |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 295 | |
tianruiming | ebb3dd0 | 2025-06-09 05:07:26 +0000 | [diff] [blame] | 296 | // Calculate infoHash using ISO_8859_1 encoding method to match qBittorrent |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 297 | String infoHash = null; |
| 298 | try { |
tianruiming | ebb3dd0 | 2025-06-09 05:07:26 +0000 | [diff] [blame] | 299 | infoHash = calculateInfoHashReencoding(target.toFile()); |
| 300 | System.out.println("InfoHash (ISO_8859_1): " + infoHash); |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 301 | } catch (Exception e) { |
| 302 | System.err.println("Warning: could not parse torrent infoHash: " + e.getMessage()); |
tianruiming | ebb3dd0 | 2025-06-09 05:07:26 +0000 | [diff] [blame] | 303 | // Fallback to direct extraction method |
| 304 | try { |
| 305 | infoHash = calculateInfoHashDirect(target.toFile()); |
| 306 | System.out.println("InfoHash (Direct): " + infoHash); |
| 307 | } catch (Exception e2) { |
| 308 | System.err.println("Warning: fallback infoHash calculation also failed: " + e2.getMessage()); |
| 309 | } |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 310 | } |
| 311 | |
root | d4959a8 | 2025-05-27 07:07:37 +0000 | [diff] [blame] | 312 | EntityManager em = emf.createEntityManager(); |
| 313 | EntityTransaction tx = em.getTransaction(); |
| 314 | try { |
| 315 | tx.begin(); |
| 316 | Seed seed = em.find(Seed.class, seedid); |
| 317 | seed.url = target.toString(); |
| 318 | em.merge(seed); |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 319 | |
| 320 | // upsert SeedHash only if we have a valid infoHash |
| 321 | if (infoHash != null) { |
| 322 | SeedHash sh = new SeedHash(); |
| 323 | sh.seedId = seedid; |
| 324 | sh.infoHash = infoHash; |
| 325 | em.merge(sh); |
| 326 | } |
root | d4959a8 | 2025-05-27 07:07:37 +0000 | [diff] [blame] | 327 | tx.commit(); |
| 328 | return 0; |
| 329 | } catch (Exception e) { |
| 330 | if (tx.isActive()) tx.rollback(); |
| 331 | return 1; |
| 332 | } finally { |
| 333 | em.close(); |
| 334 | } |
| 335 | } catch (Exception e) { |
| 336 | return 1; |
| 337 | } |
| 338 | } |
root | 0d8b11f | 2025-05-15 14:10:43 +0000 | [diff] [blame] | 339 | @Override |
Raver | aae0612 | 2025-06-05 08:13:35 +0000 | [diff] [blame] | 340 | public File GetTTorent(String seedid, String userid) { |
root | d4959a8 | 2025-05-27 07:07:37 +0000 | [diff] [blame] | 341 | EntityManager em = emf.createEntityManager(); |
| 342 | EntityTransaction tx = em.getTransaction(); |
| 343 | File file = null; |
| 344 | try { |
| 345 | Seed seed = em.find(Seed.class, seedid); |
| 346 | if (seed == null || seed.url == null) { |
| 347 | return null; |
| 348 | } |
| 349 | file = new File(seed.url); |
| 350 | if (!file.exists()) { |
| 351 | return null; |
| 352 | } |
| 353 | tx.begin(); |
| 354 | SeedDownload sd = new SeedDownload(); |
| 355 | sd.seedId = seedid; |
| 356 | sd.userId = userid; |
root | d4959a8 | 2025-05-27 07:07:37 +0000 | [diff] [blame] | 357 | LocalDateTime now = LocalDateTime.now(); |
| 358 | sd.downloadStart = now; |
| 359 | sd.downloadEnd = now; |
| 360 | em.persist(sd); |
| 361 | tx.commit(); |
| 362 | } catch (Exception e) { |
| 363 | if (tx.isActive()) tx.rollback(); |
| 364 | // ignore persistence errors and still return the file |
| 365 | } finally { |
| 366 | em.close(); |
| 367 | } |
| 368 | return file; |
| 369 | } |
root | 0d8b11f | 2025-05-15 14:10:43 +0000 | [diff] [blame] | 370 | @Override |
root | f35409f | 2025-05-19 04:41:57 +0000 | [diff] [blame] | 371 | public int AddRecord(TransRecord rd){ |
| 372 | EntityManager em = emf.createEntityManager(); |
| 373 | EntityTransaction tx = em.getTransaction(); |
| 374 | try { |
| 375 | tx.begin(); |
| 376 | em.persist(rd); |
| 377 | tx.commit(); |
TRM-coding | d5de51e | 2025-06-08 03:27:01 +0800 | [diff] [blame] | 378 | // 返回1表示插入成功 |
root | f35409f | 2025-05-19 04:41:57 +0000 | [diff] [blame] | 379 | return 1; |
| 380 | } catch (Exception e) { |
| 381 | if (tx.isActive()) tx.rollback(); |
| 382 | return -1; |
| 383 | } finally { |
| 384 | em.close(); |
| 385 | } |
| 386 | } |
tianruiming | ebb3dd0 | 2025-06-09 05:07:26 +0000 | [diff] [blame] | 387 | |
| 388 | /** |
| 389 | * Calculate infoHash by extracting the original info dictionary bytes |
| 390 | * from the torrent file, rather than re-encoding the parsed data. |
| 391 | * This method preserves the original binary representation. |
| 392 | */ |
| 393 | private String calculateInfoHashDirect(File torrentFile) throws Exception { |
| 394 | byte[] torrentData = Files.readAllBytes(torrentFile.toPath()); |
| 395 | |
| 396 | // Find the info dictionary in the raw torrent data |
| 397 | int infoStart = findInfoDictionary(torrentData); |
| 398 | if (infoStart == -1) { |
| 399 | throw new Exception("Could not find info dictionary in torrent file"); |
| 400 | } |
| 401 | |
| 402 | // Extract the info dictionary bytes directly from the original torrent |
| 403 | byte[] infoBytes = extractInfoBytes(torrentData, infoStart); |
| 404 | |
| 405 | // Debug: print first few bytes of info dict |
| 406 | System.out.print("Info dict starts with: "); |
| 407 | for (int i = 0; i < Math.min(20, infoBytes.length); i++) { |
| 408 | System.out.printf("%02x ", infoBytes[i] & 0xff); |
| 409 | } |
| 410 | System.out.println(); |
| 411 | |
| 412 | // Calculate SHA1 hash |
| 413 | MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); |
| 414 | byte[] digest = sha1.digest(infoBytes); |
| 415 | |
| 416 | // Convert to hex string |
| 417 | StringBuilder sb = new StringBuilder(); |
| 418 | for (byte b : digest) { |
| 419 | sb.append(String.format("%02x", b & 0xff)); |
| 420 | } |
| 421 | |
| 422 | return sb.toString(); |
| 423 | } |
| 424 | |
| 425 | /** |
| 426 | * Correct method using ISO_8859_1 encoding for infohash calculation |
| 427 | * This matches qBittorrent's calculation method |
| 428 | */ |
| 429 | private String calculateInfoHashReencoding(File torrentFile) throws Exception { |
| 430 | byte[] torrentData = Files.readAllBytes(torrentFile.toPath()); |
| 431 | |
| 432 | // Use ISO_8859_1 charset for infohash calculation (as per BitTorrent specification) |
| 433 | Bencode bencodeInfoHash = new Bencode(java.nio.charset.StandardCharsets.ISO_8859_1); |
| 434 | |
| 435 | @SuppressWarnings("unchecked") |
| 436 | Map<String,Object> meta = bencodeInfoHash.decode(torrentData, Type.DICTIONARY); |
| 437 | @SuppressWarnings("unchecked") |
| 438 | Map<String,Object> info = (Map<String,Object>) meta.get("info"); |
| 439 | |
| 440 | if (info == null) { |
| 441 | throw new Exception("No info dictionary found"); |
| 442 | } |
| 443 | |
| 444 | // Re-encode the info dictionary using ISO_8859_1 |
| 445 | byte[] infoBytes = bencodeInfoHash.encode(info); |
| 446 | |
| 447 | // Calculate SHA1 hash |
| 448 | MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); |
| 449 | byte[] digest = sha1.digest(infoBytes); |
| 450 | |
| 451 | StringBuilder sb = new StringBuilder(); |
| 452 | for (byte b : digest) { |
| 453 | sb.append(String.format("%02x", b & 0xff)); |
| 454 | } |
| 455 | |
| 456 | return sb.toString(); |
| 457 | } |
| 458 | |
| 459 | /** |
| 460 | * Find the position of "4:info" in the torrent data |
| 461 | */ |
| 462 | private int findInfoDictionary(byte[] data) { |
| 463 | byte[] pattern = "4:info".getBytes(); |
| 464 | |
| 465 | for (int i = 0; i <= data.length - pattern.length; i++) { |
| 466 | boolean found = true; |
| 467 | for (int j = 0; j < pattern.length; j++) { |
| 468 | if (data[i + j] != pattern[j]) { |
| 469 | found = false; |
| 470 | break; |
| 471 | } |
| 472 | } |
| 473 | if (found) { |
| 474 | return i; |
| 475 | } |
| 476 | } |
| 477 | return -1; |
| 478 | } |
| 479 | |
| 480 | /** |
| 481 | * Extract the info dictionary bytes from the original torrent data |
| 482 | */ |
| 483 | private byte[] extractInfoBytes(byte[] data, int infoStart) throws Exception { |
| 484 | // Skip "4:info" to get to the actual dictionary content |
| 485 | int dictStart = infoStart + 6; // "4:info".length() |
| 486 | |
| 487 | if (dictStart >= data.length || data[dictStart] != 'd') { |
| 488 | throw new Exception("Invalid info dictionary format"); |
| 489 | } |
| 490 | |
| 491 | // Find the matching 'e' that closes the info dictionary |
| 492 | int dictEnd = findMatchingEnd(data, dictStart); |
| 493 | if (dictEnd == -1) { |
| 494 | throw new Exception("Could not find end of info dictionary"); |
| 495 | } |
| 496 | |
| 497 | // Extract the info dictionary bytes (including 'd' and 'e') |
| 498 | int length = dictEnd - dictStart + 1; |
| 499 | byte[] infoBytes = new byte[length]; |
| 500 | System.arraycopy(data, dictStart, infoBytes, 0, length); |
| 501 | |
| 502 | return infoBytes; |
| 503 | } |
| 504 | |
| 505 | /** |
| 506 | * Find the matching 'e' for a dictionary that starts with 'd' |
| 507 | */ |
| 508 | private int findMatchingEnd(byte[] data, int start) { |
| 509 | if (start >= data.length || data[start] != 'd') { |
| 510 | return -1; |
| 511 | } |
| 512 | |
| 513 | int depth = 0; |
| 514 | int i = start; |
| 515 | |
| 516 | while (i < data.length) { |
| 517 | byte b = data[i]; |
| 518 | |
| 519 | if (b == 'd' || b == 'l') { |
| 520 | // Dictionary or list start |
| 521 | depth++; |
| 522 | i++; |
| 523 | } else if (b == 'e') { |
| 524 | // Dictionary or list end |
| 525 | depth--; |
| 526 | if (depth == 0) { |
| 527 | return i; |
| 528 | } |
| 529 | i++; |
| 530 | } else if (b == 'i') { |
| 531 | // Integer: i<number>e |
| 532 | i++; // skip 'i' |
| 533 | while (i < data.length && data[i] != 'e') { |
| 534 | i++; |
| 535 | } |
| 536 | if (i < data.length) i++; // skip 'e' |
| 537 | } else if (b >= '0' && b <= '9') { |
| 538 | // String: <length>:<string> |
| 539 | int lengthStart = i; |
| 540 | while (i < data.length && data[i] >= '0' && data[i] <= '9') { |
| 541 | i++; |
| 542 | } |
| 543 | if (i < data.length && data[i] == ':') { |
| 544 | // Parse length |
| 545 | String lengthStr = new String(data, lengthStart, i - lengthStart); |
| 546 | int length = Integer.parseInt(lengthStr); |
| 547 | i++; // skip ':' |
| 548 | i += length; // skip string content |
| 549 | } else { |
| 550 | // Invalid format |
| 551 | return -1; |
| 552 | } |
| 553 | } else { |
| 554 | // Unknown character |
| 555 | i++; |
| 556 | } |
| 557 | } |
| 558 | |
| 559 | return -1; |
| 560 | } |
TRM-coding | 83df9e2 | 2025-06-09 17:51:05 +0800 | [diff] [blame] | 561 | |
| 562 | /** |
| 563 | * 从数据库中查询所有 info_hash(hex 字符串) |
| 564 | */ |
| 565 | public List<String> getAllInfoHashes() { |
| 566 | EntityManager em = emf.createEntityManager(); |
| 567 | try { |
| 568 | return em.createQuery( |
| 569 | "SELECT sh.infoHash FROM SeedHash sh", String.class |
| 570 | ).getResultList(); |
| 571 | } finally { |
| 572 | em.close(); |
| 573 | } |
| 574 | } |
root | 33a7d95 | 2025-05-18 17:24:41 +0000 | [diff] [blame] | 575 | } |