feat: count all platforms in multi-link posts

- add PlatformSet to track unique platforms per post
- detectAllPlatformsFromRecord returns all platforms found
- posts with multiple platform links now count each platform
- remove old single-platform detection function

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Changed files
+84 -18
src
+76 -14
src/feed/filter.zig
··· 115 115 return null; 116 116 } 117 117 118 - /// detect platform from post record (checks facets and embeds) 119 - pub fn detectPlatformFromRecord(record: Record) ?stats.Platform { 118 + /// simple text-based check for music links (used for backfill) 119 + pub fn textContainsMusicLink(text: []const u8) bool { 120 + for (music_domains) |domain| { 121 + if (mem.indexOf(u8, text, domain) != null) return true; 122 + } 123 + return false; 124 + } 125 + 126 + /// set of platforms found in a post (deduplicated) 127 + pub const PlatformSet = struct { 128 + soundcloud: bool = false, 129 + bandcamp: bool = false, 130 + spotify: bool = false, 131 + plyr: bool = false, 132 + apple: bool = false, 133 + 134 + pub fn add(self: *PlatformSet, p: stats.Platform) void { 135 + switch (p) { 136 + .soundcloud => self.soundcloud = true, 137 + .bandcamp => self.bandcamp = true, 138 + .spotify => self.spotify = true, 139 + .plyr => self.plyr = true, 140 + .apple => self.apple = true, 141 + } 142 + } 143 + 144 + pub fn isEmpty(self: PlatformSet) bool { 145 + return !self.soundcloud and !self.bandcamp and !self.spotify and !self.plyr and !self.apple; 146 + } 147 + 148 + pub fn count(self: PlatformSet) u8 { 149 + var n: u8 = 0; 150 + if (self.soundcloud) n += 1; 151 + if (self.bandcamp) n += 1; 152 + if (self.spotify) n += 1; 153 + if (self.plyr) n += 1; 154 + if (self.apple) n += 1; 155 + return n; 156 + } 157 + }; 158 + 159 + /// detect all unique platforms in a post record 160 + pub fn detectAllPlatformsFromRecord(record: Record) PlatformSet { 161 + var result = PlatformSet{}; 120 162 const val: json.Value = .{ .object = record }; 121 163 122 164 // check embed.external.uri 123 165 if (zat.json.getString(val, "embed.external.uri")) |uri| { 124 - if (detectPlatform(uri)) |p| return p; 166 + if (detectPlatform(uri)) |p| result.add(p); 125 167 } 126 168 127 169 // check facets 128 - const facets = zat.json.getArray(val, "facets") orelse return null; 170 + const facets = zat.json.getArray(val, "facets") orelse return result; 129 171 for (facets) |facet| { 130 172 const features = zat.json.getArray(facet, "features") orelse continue; 131 173 for (features) |feature| { 132 174 const uri = zat.json.getString(feature, "uri") orelse continue; 133 - if (detectPlatform(uri)) |p| return p; 175 + if (detectPlatform(uri)) |p| result.add(p); 134 176 } 135 177 } 136 178 137 - return null; 138 - } 139 - 140 - /// simple text-based check for music links (used for backfill) 141 - pub fn textContainsMusicLink(text: []const u8) bool { 142 - for (music_domains) |domain| { 143 - if (mem.indexOf(u8, text, domain) != null) return true; 144 - } 145 - return false; 179 + return result; 146 180 } 147 181 148 182 // ----------------------------------------------------------------------------- ··· 195 229 try testing.expect(!textContainsMusicLink("just vibing to some tunes")); 196 230 try testing.expect(!textContainsMusicLink("")); 197 231 } 232 + 233 + test "PlatformSet" { 234 + const testing = std.testing; 235 + 236 + var set = PlatformSet{}; 237 + try testing.expect(set.isEmpty()); 238 + try testing.expectEqual(0, set.count()); 239 + 240 + set.add(.spotify); 241 + try testing.expect(!set.isEmpty()); 242 + try testing.expectEqual(1, set.count()); 243 + try testing.expect(set.spotify); 244 + try testing.expect(!set.soundcloud); 245 + 246 + // adding same platform again doesn't change count 247 + set.add(.spotify); 248 + try testing.expectEqual(1, set.count()); 249 + 250 + // adding different platforms 251 + set.add(.apple); 252 + set.add(.bandcamp); 253 + try testing.expectEqual(3, set.count()); 254 + try testing.expect(set.spotify); 255 + try testing.expect(set.apple); 256 + try testing.expect(set.bandcamp); 257 + try testing.expect(!set.soundcloud); 258 + try testing.expect(!set.plyr); 259 + }
+8 -4
src/jetstream.zig
··· 165 165 166 166 stats.get().recordMatch(); 167 167 168 - // track platform 169 - if (filter.detectPlatformFromRecord(post_record.object)) |platform| { 170 - stats.get().recordPlatform(platform); 171 - } 168 + // track all platforms in post 169 + const platforms = filter.detectAllPlatformsFromRecord(post_record.object); 170 + const s = stats.get(); 171 + if (platforms.soundcloud) s.recordPlatform(.soundcloud); 172 + if (platforms.bandcamp) s.recordPlatform(.bandcamp); 173 + if (platforms.spotify) s.recordPlatform(.spotify); 174 + if (platforms.plyr) s.recordPlatform(.plyr); 175 + if (platforms.apple) s.recordPlatform(.apple); 172 176 173 177 std.debug.print("added: {s}\n", .{at_uri.string}); 174 178 }