Commit Diff


commit - 8bb049777277d22477250c678acde6415aabca98
commit + 432e81a8c21e9e8f2c9bac7b6548fe655e0afc23
blob - /dev/null
blob + c98cf3fa8274f5e26d2e07420f57d4aec3ff9634 (mode 644)
--- /dev/null
+++ issue/comments.json
@@ -0,0 +1,272 @@
+[
+  {
+    "url": "https://api.github.com/repos/untangledco/streaming/issues/comments/2282679534",
+    "html_url": "https://github.com/untangledco/streaming/issues/31#issuecomment-2282679534",
+    "issue_url": "https://api.github.com/repos/untangledco/streaming/issues/31",
+    "id": 2282679534,
+    "node_id": "IC_kwDOL1KTdc6IDuzu",
+    "user": {
+      "login": "jacobhitchins",
+      "id": 44385069,
+      "node_id": "MDQ6VXNlcjQ0Mzg1MDY5",
+      "avatar_url": "https://avatars.githubusercontent.com/u/44385069?v=4",
+      "gravatar_id": "",
+      "url": "https://api.github.com/users/jacobhitchins",
+      "html_url": "https://github.com/jacobhitchins",
+      "followers_url": "https://api.github.com/users/jacobhitchins/followers",
+      "following_url": "https://api.github.com/users/jacobhitchins/following{/other_user}",
+      "gists_url": "https://api.github.com/users/jacobhitchins/gists{/gist_id}",
+      "starred_url": "https://api.github.com/users/jacobhitchins/starred{/owner}{/repo}",
+      "subscriptions_url": "https://api.github.com/users/jacobhitchins/subscriptions",
+      "organizations_url": "https://api.github.com/users/jacobhitchins/orgs",
+      "repos_url": "https://api.github.com/users/jacobhitchins/repos",
+      "events_url": "https://api.github.com/users/jacobhitchins/events{/privacy}",
+      "received_events_url": "https://api.github.com/users/jacobhitchins/received_events",
+      "type": "User",
+      "user_view_type": "public",
+      "site_admin": false
+    },
+    "created_at": "2024-08-11T08:48:09Z",
+    "updated_at": "2024-08-11T08:48:09Z",
+    "body": "Would this be in addition to or replacing the current samples?",
+    "author_association": "CONTRIBUTOR",
+    "reactions": {
+      "url": "https://api.github.com/repos/untangledco/streaming/issues/comments/2282679534/reactions",
+      "total_count": 0,
+      "+1": 0,
+      "-1": 0,
+      "laugh": 0,
+      "hooray": 0,
+      "confused": 0,
+      "heart": 0,
+      "rocket": 0,
+      "eyes": 0
+    },
+    "performed_via_github_app": null
+  },
+  {
+    "url": "https://api.github.com/repos/untangledco/streaming/issues/comments/2282944708",
+    "html_url": "https://github.com/untangledco/streaming/issues/31#issuecomment-2282944708",
+    "issue_url": "https://api.github.com/repos/untangledco/streaming/issues/31",
+    "id": 2282944708,
+    "node_id": "IC_kwDOL1KTdc6IEvjE",
+    "user": {
+      "login": "ollytom",
+      "id": 17470471,
+      "node_id": "MDQ6VXNlcjE3NDcwNDcx",
+      "avatar_url": "https://avatars.githubusercontent.com/u/17470471?v=4",
+      "gravatar_id": "",
+      "url": "https://api.github.com/users/ollytom",
+      "html_url": "https://github.com/ollytom",
+      "followers_url": "https://api.github.com/users/ollytom/followers",
+      "following_url": "https://api.github.com/users/ollytom/following{/other_user}",
+      "gists_url": "https://api.github.com/users/ollytom/gists{/gist_id}",
+      "starred_url": "https://api.github.com/users/ollytom/starred{/owner}{/repo}",
+      "subscriptions_url": "https://api.github.com/users/ollytom/subscriptions",
+      "organizations_url": "https://api.github.com/users/ollytom/orgs",
+      "repos_url": "https://api.github.com/users/ollytom/repos",
+      "events_url": "https://api.github.com/users/ollytom/events{/privacy}",
+      "received_events_url": "https://api.github.com/users/ollytom/received_events",
+      "type": "User",
+      "user_view_type": "public",
+      "site_admin": false
+    },
+    "created_at": "2024-08-12T00:21:01Z",
+    "updated_at": "2024-08-12T00:21:01Z",
+    "body": "> Would this be in addition to or replacing the current samples?\n\nIn addition to. Looking at the playlists in the testdata directory\neach one seems to be there to test different tags. But it's not clear\nlooking at the files what each one is really for. A comment at the top\nof each file, and/or a descriptive file name would be helpful\nespecially for someone forgetful like me :)\n",
+    "author_association": "MEMBER",
+    "reactions": {
+      "url": "https://api.github.com/repos/untangledco/streaming/issues/comments/2282944708/reactions",
+      "total_count": 0,
+      "+1": 0,
+      "-1": 0,
+      "laugh": 0,
+      "hooray": 0,
+      "confused": 0,
+      "heart": 77,
+      "rocket": 0,
+      "eyes": 0
+    },
+    "performed_via_github_app": null
+  },
+  {
+    "url": "https://api.github.com/repos/untangledco/streaming/issues/comments/2282965170",
+    "html_url": "https://github.com/untangledco/streaming/issues/31#issuecomment-2282965170",
+    "issue_url": "https://api.github.com/repos/untangledco/streaming/issues/31",
+    "id": 2282965170,
+    "node_id": "IC_kwDOL1KTdc6IE0iy",
+    "user": {
+      "login": "ollytom",
+      "id": 17470471,
+      "node_id": "MDQ6VXNlcjE3NDcwNDcx",
+      "avatar_url": "https://avatars.githubusercontent.com/u/17470471?v=4",
+      "gravatar_id": "",
+      "url": "https://api.github.com/users/ollytom",
+      "html_url": "https://github.com/ollytom",
+      "followers_url": "https://api.github.com/users/ollytom/followers",
+      "following_url": "https://api.github.com/users/ollytom/following{/other_user}",
+      "gists_url": "https://api.github.com/users/ollytom/gists{/gist_id}",
+      "starred_url": "https://api.github.com/users/ollytom/starred{/owner}{/repo}",
+      "subscriptions_url": "https://api.github.com/users/ollytom/subscriptions",
+      "organizations_url": "https://api.github.com/users/ollytom/orgs",
+      "repos_url": "https://api.github.com/users/ollytom/repos",
+      "events_url": "https://api.github.com/users/ollytom/events{/privacy}",
+      "received_events_url": "https://api.github.com/users/ollytom/received_events",
+      "type": "User",
+      "user_view_type": "public",
+      "site_admin": false
+    },
+    "created_at": "2024-08-12T00:59:59Z",
+    "updated_at": "2024-08-12T00:59:59Z",
+    "body": "> But it's not clear looking at the files what each one is really for.\n\nActually, looking at it again today, after a sleepy Sunday, I think\nit's relatively clear what each is for. For example,\ntestdata/frame_rate.m3u8 is for frame rates, with names for each (PAL,\nNTSC etc.) and testdata/resolution.m3u8 is for resolution. There's two\nwhich are unclear:\n\n- bbb.m3u8\n- tos.m3u8\n\nI've fixed these up in 7c79b05479.",
+    "author_association": "MEMBER",
+    "reactions": {
+      "url": "https://api.github.com/repos/untangledco/streaming/issues/comments/2282965170/reactions",
+      "total_count": 0,
+      "+1": 0,
+      "-1": 0,
+      "laugh": 0,
+      "hooray": 0,
+      "confused": 0,
+      "heart": 0,
+      "rocket": 0,
+      "eyes": 0
+    },
+    "performed_via_github_app": null
+  },
+  {
+    "url": "https://api.github.com/repos/untangledco/streaming/issues/comments/3120531112",
+    "html_url": "https://github.com/untangledco/streaming/issues/31#issuecomment-3120531112",
+    "issue_url": "https://api.github.com/repos/untangledco/streaming/issues/31",
+    "id": 3120531112,
+    "node_id": "IC_kwDOL1KTdc65_4ao",
+    "user": {
+      "login": "dipoll",
+      "id": 11376739,
+      "node_id": "MDQ6VXNlcjExMzc2NzM5",
+      "avatar_url": "https://avatars.githubusercontent.com/u/11376739?v=4",
+      "gravatar_id": "",
+      "url": "https://api.github.com/users/dipoll",
+      "html_url": "https://github.com/dipoll",
+      "followers_url": "https://api.github.com/users/dipoll/followers",
+      "following_url": "https://api.github.com/users/dipoll/following{/other_user}",
+      "gists_url": "https://api.github.com/users/dipoll/gists{/gist_id}",
+      "starred_url": "https://api.github.com/users/dipoll/starred{/owner}{/repo}",
+      "subscriptions_url": "https://api.github.com/users/dipoll/subscriptions",
+      "organizations_url": "https://api.github.com/users/dipoll/orgs",
+      "repos_url": "https://api.github.com/users/dipoll/repos",
+      "events_url": "https://api.github.com/users/dipoll/events{/privacy}",
+      "received_events_url": "https://api.github.com/users/dipoll/received_events",
+      "type": "User",
+      "user_view_type": "public",
+      "site_admin": false
+    },
+    "created_at": "2025-07-25T22:12:39Z",
+    "updated_at": "2025-07-25T22:12:39Z",
+    "body": "Sorry for bothering, but are you planning to add parsing #EXTINF with additional key=value pairs?",
+    "author_association": "NONE",
+    "reactions": {
+      "url": "https://api.github.com/repos/untangledco/streaming/issues/comments/3120531112/reactions",
+      "total_count": 0,
+      "+1": 0,
+      "-1": 0,
+      "laugh": 0,
+      "hooray": 0,
+      "confused": 0,
+      "heart": 0,
+      "rocket": 0,
+      "eyes": 0
+    },
+    "performed_via_github_app": null
+  },
+  {
+    "url": "https://api.github.com/repos/untangledco/streaming/issues/comments/3123517731",
+    "html_url": "https://github.com/untangledco/streaming/issues/31#issuecomment-3123517731",
+    "issue_url": "https://api.github.com/repos/untangledco/streaming/issues/31",
+    "id": 3123517731,
+    "node_id": "IC_kwDOL1KTdc66LRkj",
+    "user": {
+      "login": "ollytom",
+      "id": 17470471,
+      "node_id": "MDQ6VXNlcjE3NDcwNDcx",
+      "avatar_url": "https://avatars.githubusercontent.com/u/17470471?v=4",
+      "gravatar_id": "",
+      "url": "https://api.github.com/users/ollytom",
+      "html_url": "https://github.com/ollytom",
+      "followers_url": "https://api.github.com/users/ollytom/followers",
+      "following_url": "https://api.github.com/users/ollytom/following{/other_user}",
+      "gists_url": "https://api.github.com/users/ollytom/gists{/gist_id}",
+      "starred_url": "https://api.github.com/users/ollytom/starred{/owner}{/repo}",
+      "subscriptions_url": "https://api.github.com/users/ollytom/subscriptions",
+      "organizations_url": "https://api.github.com/users/ollytom/orgs",
+      "repos_url": "https://api.github.com/users/ollytom/repos",
+      "events_url": "https://api.github.com/users/ollytom/events{/privacy}",
+      "received_events_url": "https://api.github.com/users/ollytom/received_events",
+      "type": "User",
+      "user_view_type": "public",
+      "site_admin": false
+    },
+    "created_at": "2025-07-26T23:14:28Z",
+    "updated_at": "2025-07-26T23:14:28Z",
+    "body": "@dipoll Oh I didn't realise there was any more info other than the segment duration from the `EXTINF` tag! Created issue #41 to track extracting the data",
+    "author_association": "MEMBER",
+    "reactions": {
+      "url": "https://api.github.com/repos/untangledco/streaming/issues/comments/3123517731/reactions",
+      "total_count": 0,
+      "+1": 0,
+      "-1": 0,
+      "laugh": 0,
+      "hooray": 0,
+      "confused": 0,
+      "heart": 0,
+      "rocket": 0,
+      "eyes": 0
+    },
+    "performed_via_github_app": null
+  },
+  {
+    "url": "https://api.github.com/repos/untangledco/streaming/issues/comments/3126817916",
+    "html_url": "https://github.com/untangledco/streaming/issues/31#issuecomment-3126817916",
+    "issue_url": "https://api.github.com/repos/untangledco/streaming/issues/31",
+    "id": 3126817916,
+    "node_id": "IC_kwDOL1KTdc66X3R8",
+    "user": {
+      "login": "ollytom",
+      "id": 17470471,
+      "node_id": "MDQ6VXNlcjE3NDcwNDcx",
+      "avatar_url": "https://avatars.githubusercontent.com/u/17470471?v=4",
+      "gravatar_id": "",
+      "url": "https://api.github.com/users/ollytom",
+      "html_url": "https://github.com/ollytom",
+      "followers_url": "https://api.github.com/users/ollytom/followers",
+      "following_url": "https://api.github.com/users/ollytom/following{/other_user}",
+      "gists_url": "https://api.github.com/users/ollytom/gists{/gist_id}",
+      "starred_url": "https://api.github.com/users/ollytom/starred{/owner}{/repo}",
+      "subscriptions_url": "https://api.github.com/users/ollytom/subscriptions",
+      "organizations_url": "https://api.github.com/users/ollytom/orgs",
+      "repos_url": "https://api.github.com/users/ollytom/repos",
+      "events_url": "https://api.github.com/users/ollytom/events{/privacy}",
+      "received_events_url": "https://api.github.com/users/ollytom/received_events",
+      "type": "User",
+      "user_view_type": "public",
+      "site_admin": false
+    },
+    "created_at": "2025-07-28T11:36:35Z",
+    "updated_at": "2025-07-28T11:36:35Z",
+    "body": "@dipoll should be fixed now - see issue #41 for detail on the thinking :)",
+    "author_association": "MEMBER",
+    "reactions": {
+      "url": "https://api.github.com/repos/untangledco/streaming/issues/comments/3126817916/reactions",
+      "total_count": 0,
+      "+1": 0,
+      "-1": 0,
+      "laugh": 0,
+      "hooray": 0,
+      "confused": 0,
+      "heart": 0,
+      "rocket": 0,
+      "eyes": 0
+    },
+    "performed_via_github_app": null
+  }
+]
blob - b3ac102e904d55b4fa1ab09848080bec5095b9e9
blob + 3a9f812b3b00d9f13d54eca7d686b98be485baee
--- issue/issue.go
+++ issue/issue.go
@@ -616,8 +616,8 @@ type Comment struct {
 }
 
 type Reactions struct {
-	PlusOne  int
-	MinusOne int
+	PlusOne  int `json:"+1"`
+	MinusOne int `json:"-1"`
 	Laugh    int
 	Confused int
 	Heart    int
blob - /dev/null
blob + a877ea0e120b13ddf565ac268f6693cb4c1a267b (mode 644)
--- /dev/null
+++ issue/gh.go
@@ -0,0 +1,146 @@
+package main
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
+	"time"
+)
+
+const defaultBaseURL = "https://api.github.com"
+
+type Client struct {
+	baseURL string
+	Token   string
+	*http.Client
+}
+
+type GIssue struct {
+	Number  int
+	Title   string
+	Creator struct {
+		Name string `json:"login"`
+	} `json:"user"`
+	Assignee struct {
+		Name string `json:"login"`
+	}
+	Labels    []string
+	State     string
+	Created   time.Time `json:"created_at"`
+	Updated   time.Time `json:"updated_at"`
+	Closed    time.Time `json:"closed_at"`
+	HTMLURL   string    `json:"html_url"`
+	Body      string
+	Comments  int
+	Reactions Reactions
+}
+
+func printGIssue(w io.Writer, issue *GIssue) error {
+	buf := &bytes.Buffer{}
+	fmt.Fprintln(buf, "Title:", issue.Title)
+	fmt.Fprintln(buf, "State:", issue.State)
+	fmt.Fprintln(buf, "From:", issue.Creator.Name)
+	fmt.Fprintln(buf, "Date:", issue.Updated.Format(time.DateTime))
+	if !issue.Closed.IsZero() {
+		fmt.Fprintln(buf, "Closed:", issue.Closed.Format(time.DateTime))
+	}
+	fmt.Fprintln(buf, "Assignee:", issue.Assignee.Name)
+	fmt.Fprintln(buf, "Labels:", strings.Join(issue.Labels, " "))
+	if issue.Reactions.String() != "" {
+		fmt.Fprintln(buf, "Reactions:", issue.Reactions)
+	}
+	fmt.Fprintln(buf, "URL:", issue.HTMLURL)
+	fmt.Fprintln(buf)
+	fmt.Fprintln(buf, strings.TrimSpace(issue.Body))
+	fmt.Fprintln(buf)
+	_, err := io.Copy(w, buf)
+	return err
+}
+
+type GComment struct {
+	Created time.Time `json:"created_at"`
+	Updated time.Time `json:"updated_at"`
+	Body    string
+	User    struct {
+		Name string `json:"login"`
+	}
+	Reactions Reactions
+}
+
+func printComments(w io.Writer, comments []GComment) error {
+	buf := &bytes.Buffer{}
+	for _, c := range comments {
+		fmt.Fprintf(buf, "Comment by %s (%s)\n", c.User.Name, c.Updated.Format(time.DateTime))
+		fmt.Fprintln(buf)
+		fmt.Fprintln(buf, strings.TrimSpace(c.Body))
+		if c.Reactions.String() != "" {
+			fmt.Fprintln(buf)
+			fmt.Fprintln(buf, "Reactions:", c.Reactions)
+		}
+		fmt.Fprintln(buf)
+	}
+	_, err := io.Copy(w, buf)
+	return err
+}
+
+func (c *Client) LookupIssue(number int) (*Issue, error) {
+	var issue Issue
+	path := fmt.Sprintf("/issues/%d", number)
+	err := c.get(path, &issue)
+	return &issue, err
+}
+
+func (c *Client) Issues(owner, repo string) ([]Issue, error) {
+	var issues []Issue
+	err := c.get("/issues", &issues)
+	return issues, err
+}
+
+func (c *Client) Comments(issue int) ([]Comment, error) {
+	var comments []Comment
+	path := fmt.Sprintf("/issues/%d/comments", issue)
+	err := c.get(path, &comments)
+	return comments, err
+}
+
+func (c *Client) get(path string, v any) error {
+	u := c.baseURL + path
+	req, err := http.NewRequest(http.MethodGet, u, nil)
+	if err != nil {
+		return err
+	}
+
+	resp, err := c.do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	// TODO: check HTTP status, decode any error messages from body
+
+	if err := json.NewDecoder(resp.Body).Decode(v); err != nil {
+		return fmt.Errorf("decode response: %w", err)
+	}
+	return nil
+}
+
+func (c *Client) post(path string, body io.Reader) (*http.Response, error) {
+	u := c.baseURL + path
+	req, err := http.NewRequest(http.MethodPost, u, body)
+	if err != nil {
+		return nil, err
+	}
+	return c.do(req)
+}
+
+func (c *Client) do(req *http.Request) (*http.Response, error) {
+	if c.Client == nil {
+		c.Client = http.DefaultClient
+	}
+	if c.Token != "" {
+		req.Header.Set("Authorization", "Bearer "+c.Token)
+	}
+	return c.Do(req)
+}
blob - /dev/null
blob + 62986ef887a033a167612c9f9a553c4aea78268d (mode 644)
--- /dev/null
+++ issue/gh_test.go
@@ -0,0 +1,33 @@
+package main
+
+import (
+	"encoding/json"
+	"io"
+	"os"
+	"testing"
+)
+
+func TestReadIssues(t *testing.T) {
+	f, err := os.Open("issues.json")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer f.Close()
+	var issues []GIssue
+	if err := json.NewDecoder(f).Decode(&issues); err != nil {
+		t.Fatal(err)
+	}
+	for _, is := range issues {
+		printGIssue(io.Discard, &is)
+	}
+
+	b, err := os.ReadFile("comments.json")
+	if err != nil {
+		t.Fatal(err)
+	}
+	var comments []GComment
+	if err := json.Unmarshal(b, &comments); err != nil {
+		t.Fatal(err)
+	}
+	printComments(io.Discard, comments)
+}
blob - /dev/null
blob + b066d06f00328cd6866a1c45be48f2a5919ddc37 (mode 644)
--- /dev/null
+++ issue/issues.json
@@ -0,0 +1,591 @@
+[
+  {
+    "url": "https://api.github.com/repos/untangledco/streaming/issues/43",
+    "repository_url": "https://api.github.com/repos/untangledco/streaming",
+    "labels_url": "https://api.github.com/repos/untangledco/streaming/issues/43/labels{/name}",
+    "comments_url": "https://api.github.com/repos/untangledco/streaming/issues/43/comments",
+    "events_url": "https://api.github.com/repos/untangledco/streaming/issues/43/events",
+    "html_url": "https://github.com/untangledco/streaming/pull/43",
+    "id": 3387677104,
+    "node_id": "PR_kwDOL1KTdc6nDd3X",
+    "number": 43,
+    "title": "Fixed issues with parsing, added parsing of IFrames",
+    "user": {
+      "login": "dipoll",
+      "id": 11376739,
+      "node_id": "MDQ6VXNlcjExMzc2NzM5",
+      "avatar_url": "https://avatars.githubusercontent.com/u/11376739?v=4",
+      "gravatar_id": "",
+      "url": "https://api.github.com/users/dipoll",
+      "html_url": "https://github.com/dipoll",
+      "followers_url": "https://api.github.com/users/dipoll/followers",
+      "following_url": "https://api.github.com/users/dipoll/following{/other_user}",
+      "gists_url": "https://api.github.com/users/dipoll/gists{/gist_id}",
+      "starred_url": "https://api.github.com/users/dipoll/starred{/owner}{/repo}",
+      "subscriptions_url": "https://api.github.com/users/dipoll/subscriptions",
+      "organizations_url": "https://api.github.com/users/dipoll/orgs",
+      "repos_url": "https://api.github.com/users/dipoll/repos",
+      "events_url": "https://api.github.com/users/dipoll/events{/privacy}",
+      "received_events_url": "https://api.github.com/users/dipoll/received_events",
+      "type": "User",
+      "user_view_type": "public",
+      "site_admin": false
+    },
+    "labels": [],
+    "state": "open",
+    "locked": false,
+    "assignee": null,
+    "assignees": [],
+    "milestone": null,
+    "comments": 2,
+    "created_at": "2025-09-05T14:02:33Z",
+    "updated_at": "2025-10-03T19:00:30Z",
+    "closed_at": null,
+    "author_association": "NONE",
+    "type": null,
+    "active_lock_reason": null,
+    "draft": false,
+    "pull_request": {
+      "url": "https://api.github.com/repos/untangledco/streaming/pulls/43",
+      "html_url": "https://github.com/untangledco/streaming/pull/43",
+      "diff_url": "https://github.com/untangledco/streaming/pull/43.diff",
+      "patch_url": "https://github.com/untangledco/streaming/pull/43.patch",
+      "merged_at": null
+    },
+    "body": "* Added IFrame streams as a separate slice attribute.\r\n* ProgramID parsing, because nowadays, HLS version 3 is very common\r\n* Map parsing with @ sign",
+    "closed_by": null,
+    "reactions": {
+      "url": "https://api.github.com/repos/untangledco/streaming/issues/43/reactions",
+      "total_count": 0,
+      "+1": 0,
+      "-1": 0,
+      "laugh": 0,
+      "hooray": 0,
+      "confused": 0,
+      "heart": 77,
+      "rocket": 0,
+      "eyes": 0
+    },
+    "timeline_url": "https://api.github.com/repos/untangledco/streaming/issues/43/timeline",
+    "performed_via_github_app": null,
+    "state_reason": null
+  },
+  {
+    "url": "https://api.github.com/repos/untangledco/streaming/issues/40",
+    "repository_url": "https://api.github.com/repos/untangledco/streaming",
+    "labels_url": "https://api.github.com/repos/untangledco/streaming/issues/40/labels{/name}",
+    "comments_url": "https://api.github.com/repos/untangledco/streaming/issues/40/comments",
+    "events_url": "https://api.github.com/repos/untangledco/streaming/issues/40/events",
+    "html_url": "https://github.com/untangledco/streaming/issues/40",
+    "id": 3241836085,
+    "node_id": "I_kwDOL1KTdc7BOn41",
+    "number": 40,
+    "title": "pcap: what is the GlobalHeader.Network field?",
+    "user": {
+      "login": "ollytom",
+      "id": 17470471,
+      "node_id": "MDQ6VXNlcjE3NDcwNDcx",
+      "avatar_url": "https://avatars.githubusercontent.com/u/17470471?v=4",
+      "gravatar_id": "",
+      "url": "https://api.github.com/users/ollytom",
+      "html_url": "https://github.com/ollytom",
+      "followers_url": "https://api.github.com/users/ollytom/followers",
+      "following_url": "https://api.github.com/users/ollytom/following{/other_user}",
+      "gists_url": "https://api.github.com/users/ollytom/gists{/gist_id}",
+      "starred_url": "https://api.github.com/users/ollytom/starred{/owner}{/repo}",
+      "subscriptions_url": "https://api.github.com/users/ollytom/subscriptions",
+      "organizations_url": "https://api.github.com/users/ollytom/orgs",
+      "repos_url": "https://api.github.com/users/ollytom/repos",
+      "events_url": "https://api.github.com/users/ollytom/events{/privacy}",
+      "received_events_url": "https://api.github.com/users/ollytom/received_events",
+      "type": "User",
+      "user_view_type": "public",
+      "site_admin": false
+    },
+    "labels": [],
+    "state": "open",
+    "locked": false,
+    "assignee": null,
+    "assignees": [],
+    "milestone": null,
+    "comments": 0,
+    "created_at": "2025-07-18T04:44:52Z",
+    "updated_at": "2025-07-18T04:44:52Z",
+    "closed_at": null,
+    "author_association": "MEMBER",
+    "type": null,
+    "active_lock_reason": null,
+    "sub_issues_summary": {
+      "total": 0,
+      "completed": 0,
+      "percent_completed": 0
+    },
+    "issue_dependencies_summary": {
+      "blocked_by": 0,
+      "total_blocked_by": 0,
+      "blocking": 0,
+      "total_blocking": 0
+    },
+    "body": "It's a uint32... what does this number represent?\nLast time I looked at the pcap savefile spec I seem to remember it wasn't really a uint32 at all; it represented something to do with the packet's link layer, and its fields happened to be 4 bytes long (which fits in a uint32).",
+    "closed_by": null,
+    "reactions": {
+      "url": "https://api.github.com/repos/untangledco/streaming/issues/40/reactions",
+      "total_count": 0,
+      "+1": 0,
+      "-1": 0,
+      "laugh": 0,
+      "hooray": 0,
+      "confused": 0,
+      "heart": 0,
+      "rocket": 0,
+      "eyes": 0
+    },
+    "timeline_url": "https://api.github.com/repos/untangledco/streaming/issues/40/timeline",
+    "performed_via_github_app": null,
+    "state_reason": null
+  },
+  {
+    "url": "https://api.github.com/repos/untangledco/streaming/issues/33",
+    "repository_url": "https://api.github.com/repos/untangledco/streaming",
+    "labels_url": "https://api.github.com/repos/untangledco/streaming/issues/33/labels{/name}",
+    "comments_url": "https://api.github.com/repos/untangledco/streaming/issues/33/comments",
+    "events_url": "https://api.github.com/repos/untangledco/streaming/issues/33/events",
+    "html_url": "https://github.com/untangledco/streaming/issues/33",
+    "id": 2459417610,
+    "node_id": "I_kwDOL1KTdc6Sl7wK",
+    "number": 33,
+    "title": "mpegts: ideas for applications, tooling",
+    "user": {
+      "login": "ollytom",
+      "id": 17470471,
+      "node_id": "MDQ6VXNlcjE3NDcwNDcx",
+      "avatar_url": "https://avatars.githubusercontent.com/u/17470471?v=4",
+      "gravatar_id": "",
+      "url": "https://api.github.com/users/ollytom",
+      "html_url": "https://github.com/ollytom",
+      "followers_url": "https://api.github.com/users/ollytom/followers",
+      "following_url": "https://api.github.com/users/ollytom/following{/other_user}",
+      "gists_url": "https://api.github.com/users/ollytom/gists{/gist_id}",
+      "starred_url": "https://api.github.com/users/ollytom/starred{/owner}{/repo}",
+      "subscriptions_url": "https://api.github.com/users/ollytom/subscriptions",
+      "organizations_url": "https://api.github.com/users/ollytom/orgs",
+      "repos_url": "https://api.github.com/users/ollytom/repos",
+      "events_url": "https://api.github.com/users/ollytom/events{/privacy}",
+      "received_events_url": "https://api.github.com/users/ollytom/received_events",
+      "type": "User",
+      "user_view_type": "public",
+      "site_admin": false
+    },
+    "labels": [],
+    "state": "open",
+    "locked": false,
+    "assignee": null,
+    "assignees": [],
+    "milestone": null,
+    "comments": 0,
+    "created_at": "2024-08-11T04:34:47Z",
+    "updated_at": "2024-08-11T04:34:47Z",
+    "closed_at": null,
+    "author_association": "MEMBER",
+    "type": null,
+    "active_lock_reason": null,
+    "sub_issues_summary": {
+      "total": 0,
+      "completed": 0,
+      "percent_completed": 0
+    },
+    "issue_dependencies_summary": {
+      "blocked_by": 0,
+      "total_blocked_by": 0,
+      "blocking": 0,
+      "total_blocking": 0
+    },
+    "body": "https://github.com/LTNGlobal-opensource/ltntstools",
+    "closed_by": null,
+    "reactions": {
+      "url": "https://api.github.com/repos/untangledco/streaming/issues/33/reactions",
+      "total_count": 0,
+      "+1": 0,
+      "-1": 0,
+      "laugh": 0,
+      "hooray": 0,
+      "confused": 0,
+      "heart": 0,
+      "rocket": 0,
+      "eyes": 0
+    },
+    "timeline_url": "https://api.github.com/repos/untangledco/streaming/issues/33/timeline",
+    "performed_via_github_app": null,
+    "state_reason": null
+  },
+  {
+    "url": "https://api.github.com/repos/untangledco/streaming/issues/31",
+    "repository_url": "https://api.github.com/repos/untangledco/streaming",
+    "labels_url": "https://api.github.com/repos/untangledco/streaming/issues/31/labels{/name}",
+    "comments_url": "https://api.github.com/repos/untangledco/streaming/issues/31/comments",
+    "events_url": "https://api.github.com/repos/untangledco/streaming/issues/31/events",
+    "html_url": "https://github.com/untangledco/streaming/issues/31",
+    "id": 2446156154,
+    "node_id": "I_kwDOL1KTdc6RzWF6",
+    "number": 31,
+    "title": "m3u8: import Apple's test samples",
+    "user": {
+      "login": "ollytom",
+      "id": 17470471,
+      "node_id": "MDQ6VXNlcjE3NDcwNDcx",
+      "avatar_url": "https://avatars.githubusercontent.com/u/17470471?v=4",
+      "gravatar_id": "",
+      "url": "https://api.github.com/users/ollytom",
+      "html_url": "https://github.com/ollytom",
+      "followers_url": "https://api.github.com/users/ollytom/followers",
+      "following_url": "https://api.github.com/users/ollytom/following{/other_user}",
+      "gists_url": "https://api.github.com/users/ollytom/gists{/gist_id}",
+      "starred_url": "https://api.github.com/users/ollytom/starred{/owner}{/repo}",
+      "subscriptions_url": "https://api.github.com/users/ollytom/subscriptions",
+      "organizations_url": "https://api.github.com/users/ollytom/orgs",
+      "repos_url": "https://api.github.com/users/ollytom/repos",
+      "events_url": "https://api.github.com/users/ollytom/events{/privacy}",
+      "received_events_url": "https://api.github.com/users/ollytom/received_events",
+      "type": "User",
+      "user_view_type": "public",
+      "site_admin": false
+    },
+    "labels": [],
+    "state": "open",
+    "locked": false,
+    "assignee": null,
+    "assignees": [],
+    "milestone": null,
+    "comments": 6,
+    "created_at": "2024-08-03T06:18:25Z",
+    "updated_at": "2025-07-28T11:36:35Z",
+    "closed_at": null,
+    "author_association": "MEMBER",
+    "type": null,
+    "active_lock_reason": null,
+    "sub_issues_summary": {
+      "total": 0,
+      "completed": 0,
+      "percent_completed": 0
+    },
+    "issue_dependencies_summary": {
+      "blocked_by": 0,
+      "total_blocked_by": 0,
+      "blocking": 0,
+      "total_blocking": 0
+    },
+    "body": "Apple - you know, the one who basically \"own\" the HLS spec - provide some m3u8 samples at\nhttps://developer.apple.com/streaming/examples/\n\nThese would be good to have in testdata. I saw that Shaka Player have the examples in their test suite.",
+    "closed_by": null,
+    "reactions": {
+      "url": "https://api.github.com/repos/untangledco/streaming/issues/31/reactions",
+      "total_count": 0,
+      "+1": 0,
+      "-1": 0,
+      "laugh": 0,
+      "hooray": 0,
+      "confused": 0,
+      "heart": 0,
+      "rocket": 0,
+      "eyes": 0
+    },
+    "timeline_url": "https://api.github.com/repos/untangledco/streaming/issues/31/timeline",
+    "performed_via_github_app": null,
+    "state_reason": null
+  },
+  {
+    "url": "https://api.github.com/repos/untangledco/streaming/issues/28",
+    "repository_url": "https://api.github.com/repos/untangledco/streaming",
+    "labels_url": "https://api.github.com/repos/untangledco/streaming/issues/28/labels{/name}",
+    "comments_url": "https://api.github.com/repos/untangledco/streaming/issues/28/comments",
+    "events_url": "https://api.github.com/repos/untangledco/streaming/issues/28/events",
+    "html_url": "https://github.com/untangledco/streaming/issues/28",
+    "id": 2383403962,
+    "node_id": "I_kwDOL1KTdc6OD9u6",
+    "number": 28,
+    "title": "scte35: support decoding splice_schedule command type",
+    "user": {
+      "login": "ollytom",
+      "id": 17470471,
+      "node_id": "MDQ6VXNlcjE3NDcwNDcx",
+      "avatar_url": "https://avatars.githubusercontent.com/u/17470471?v=4",
+      "gravatar_id": "",
+      "url": "https://api.github.com/users/ollytom",
+      "html_url": "https://github.com/ollytom",
+      "followers_url": "https://api.github.com/users/ollytom/followers",
+      "following_url": "https://api.github.com/users/ollytom/following{/other_user}",
+      "gists_url": "https://api.github.com/users/ollytom/gists{/gist_id}",
+      "starred_url": "https://api.github.com/users/ollytom/starred{/owner}{/repo}",
+      "subscriptions_url": "https://api.github.com/users/ollytom/subscriptions",
+      "organizations_url": "https://api.github.com/users/ollytom/orgs",
+      "repos_url": "https://api.github.com/users/ollytom/repos",
+      "events_url": "https://api.github.com/users/ollytom/events{/privacy}",
+      "received_events_url": "https://api.github.com/users/ollytom/received_events",
+      "type": "User",
+      "user_view_type": "public",
+      "site_admin": false
+    },
+    "labels": [],
+    "state": "open",
+    "locked": false,
+    "assignee": null,
+    "assignees": [],
+    "milestone": null,
+    "comments": 2,
+    "created_at": "2024-07-01T10:03:58Z",
+    "updated_at": "2024-11-30T02:30:57Z",
+    "closed_at": null,
+    "author_association": "MEMBER",
+    "type": null,
+    "active_lock_reason": null,
+    "sub_issues_summary": {
+      "total": 0,
+      "completed": 0,
+      "percent_completed": 0
+    },
+    "issue_dependencies_summary": {
+      "blocked_by": 0,
+      "total_blocked_by": 0,
+      "blocking": 0,
+      "total_blocking": 0
+    },
+    "body": "We support encoding splice_schedule commands (see `packEvents`), but we don't support decoding them yet. For consistency, we could have two functions, `unpackEvent` and `unpackEvents` in command.go after `packEvent`.\n\nWe'll need accompanying tests in command_test.go. For example, a test could encode then decode the same splice_schedule command then test equality of the returned `[]Event`.",
+    "closed_by": null,
+    "reactions": {
+      "url": "https://api.github.com/repos/untangledco/streaming/issues/28/reactions",
+      "total_count": 0,
+      "+1": 0,
+      "-1": 0,
+      "laugh": 0,
+      "hooray": 0,
+      "confused": 0,
+      "heart": 0,
+      "rocket": 0,
+      "eyes": 0
+    },
+    "timeline_url": "https://api.github.com/repos/untangledco/streaming/issues/28/timeline",
+    "performed_via_github_app": null,
+    "state_reason": null
+  },
+  {
+    "url": "https://api.github.com/repos/untangledco/streaming/issues/20",
+    "repository_url": "https://api.github.com/repos/untangledco/streaming",
+    "labels_url": "https://api.github.com/repos/untangledco/streaming/issues/20/labels{/name}",
+    "comments_url": "https://api.github.com/repos/untangledco/streaming/issues/20/comments",
+    "events_url": "https://api.github.com/repos/untangledco/streaming/issues/20/events",
+    "html_url": "https://github.com/untangledco/streaming/issues/20",
+    "id": 2337150284,
+    "node_id": "I_kwDOL1KTdc6LThVM",
+    "number": 20,
+    "title": "m3u8: hlssdump? End-to-end package test",
+    "user": {
+      "login": "ollytom",
+      "id": 17470471,
+      "node_id": "MDQ6VXNlcjE3NDcwNDcx",
+      "avatar_url": "https://avatars.githubusercontent.com/u/17470471?v=4",
+      "gravatar_id": "",
+      "url": "https://api.github.com/users/ollytom",
+      "html_url": "https://github.com/ollytom",
+      "followers_url": "https://api.github.com/users/ollytom/followers",
+      "following_url": "https://api.github.com/users/ollytom/following{/other_user}",
+      "gists_url": "https://api.github.com/users/ollytom/gists{/gist_id}",
+      "starred_url": "https://api.github.com/users/ollytom/starred{/owner}{/repo}",
+      "subscriptions_url": "https://api.github.com/users/ollytom/subscriptions",
+      "organizations_url": "https://api.github.com/users/ollytom/orgs",
+      "repos_url": "https://api.github.com/users/ollytom/repos",
+      "events_url": "https://api.github.com/users/ollytom/events{/privacy}",
+      "received_events_url": "https://api.github.com/users/ollytom/received_events",
+      "type": "User",
+      "user_view_type": "public",
+      "site_admin": false
+    },
+    "labels": [],
+    "state": "open",
+    "locked": false,
+    "assignee": null,
+    "assignees": [],
+    "milestone": null,
+    "comments": 0,
+    "created_at": "2024-06-06T01:59:36Z",
+    "updated_at": "2024-06-06T01:59:36Z",
+    "closed_at": null,
+    "author_association": "MEMBER",
+    "type": null,
+    "active_lock_reason": null,
+    "sub_issues_summary": {
+      "total": 0,
+      "completed": 0,
+      "percent_completed": 0
+    },
+    "issue_dependencies_summary": {
+      "blocked_by": 0,
+      "total_blocked_by": 0,
+      "blocking": 0,
+      "total_blocking": 0
+    },
+    "body": "[hls.js] maintains a list of active HLS streams available over the Internet at \nhttps://raw.githubusercontent.com/video-dev/hls.js/v1.5.11/tests/test-streams.js\n\nWe have unit tests and there's a bunch of code we're not testing. But it would be a good exercise in dev tooling development to write a real-world application which exercises our m3u8 package more thoroughly. \nFrom there that focusses efforts on where in the package we should write tests.\n\nFor example, a tool which reads a master HLS playlist, and writes the contents of each media playlist to the filesystem.\nThat can involve decoding then re-encoding the playlist. If we hit things like tags or attributes  we don't recognise or can't handle well, we can terminate the program. This also lets us spot bad error messages from our package.\n\n[hls.js]: https://hlsjs.video-dev.org",
+    "closed_by": null,
+    "reactions": {
+      "url": "https://api.github.com/repos/untangledco/streaming/issues/20/reactions",
+      "total_count": 0,
+      "+1": 0,
+      "-1": 0,
+      "laugh": 0,
+      "hooray": 0,
+      "confused": 0,
+      "heart": 0,
+      "rocket": 0,
+      "eyes": 0
+    },
+    "timeline_url": "https://api.github.com/repos/untangledco/streaming/issues/20/timeline",
+    "performed_via_github_app": null,
+    "state_reason": null
+  },
+  {
+    "url": "https://api.github.com/repos/untangledco/streaming/issues/19",
+    "repository_url": "https://api.github.com/repos/untangledco/streaming",
+    "labels_url": "https://api.github.com/repos/untangledco/streaming/issues/19/labels{/name}",
+    "comments_url": "https://api.github.com/repos/untangledco/streaming/issues/19/comments",
+    "events_url": "https://api.github.com/repos/untangledco/streaming/issues/19/events",
+    "html_url": "https://github.com/untangledco/streaming/issues/19",
+    "id": 2332869135,
+    "node_id": "I_kwDOL1KTdc6LDMIP",
+    "number": 19,
+    "title": "m3u8: Finish TODOs in writeDateRange()",
+    "user": {
+      "login": "ollytom",
+      "id": 17470471,
+      "node_id": "MDQ6VXNlcjE3NDcwNDcx",
+      "avatar_url": "https://avatars.githubusercontent.com/u/17470471?v=4",
+      "gravatar_id": "",
+      "url": "https://api.github.com/users/ollytom",
+      "html_url": "https://github.com/ollytom",
+      "followers_url": "https://api.github.com/users/ollytom/followers",
+      "following_url": "https://api.github.com/users/ollytom/following{/other_user}",
+      "gists_url": "https://api.github.com/users/ollytom/gists{/gist_id}",
+      "starred_url": "https://api.github.com/users/ollytom/starred{/owner}{/repo}",
+      "subscriptions_url": "https://api.github.com/users/ollytom/subscriptions",
+      "organizations_url": "https://api.github.com/users/ollytom/orgs",
+      "repos_url": "https://api.github.com/users/ollytom/repos",
+      "events_url": "https://api.github.com/users/ollytom/events{/privacy}",
+      "received_events_url": "https://api.github.com/users/ollytom/received_events",
+      "type": "User",
+      "user_view_type": "public",
+      "site_admin": false
+    },
+    "labels": [],
+    "state": "open",
+    "locked": false,
+    "assignee": null,
+    "assignees": [],
+    "milestone": null,
+    "comments": 0,
+    "created_at": "2024-06-04T08:19:01Z",
+    "updated_at": "2024-06-04T08:19:01Z",
+    "closed_at": null,
+    "author_association": "MEMBER",
+    "type": null,
+    "active_lock_reason": null,
+    "sub_issues_summary": {
+      "total": 0,
+      "completed": 0,
+      "percent_completed": 0
+    },
+    "issue_dependencies_summary": {
+      "blocked_by": 0,
+      "total_blocked_by": 0,
+      "blocking": 0,
+      "total_blocking": 0
+    },
+    "body": "Fields of DateRange that we're not writing out as text include:\n\n- Duration, Planned\n- Custom attributes\n- CueCommand\n\n## CueCommand\n\nNeed to work out under which conditions to write it out. If CuiIn or CueOut are set, we cannot write out CueCommand as that's invalid.\n\nAnother bit of error handling might be to check that CueCommand and/or CueIn/Out have a valid value set in the Splice.Type.\n\n## Custom\n\nWe'll need a type switch on the value, and error out if we get anything we can't reliably turn into text.\nI don't think we'll be able quoted versus unquoted strings. Quick sketch:\n\n\tswitch v.(type) {\n\tcase float32:\n\t\treturn fmt.Sprintf(\"%03f\"...\n\tcase string:\n\t\treturn fmt.Sprintf(\"%q\" ...)\n\tcase int:\n\t\treturn fmt.Sprintf(\"%d\", ...\n\tdefault:\n\t\treturn fmt.Errorf(\"attribute %s: cannot marshal %T to text\", v)\n\t}",
+    "closed_by": null,
+    "reactions": {
+      "url": "https://api.github.com/repos/untangledco/streaming/issues/19/reactions",
+      "total_count": 0,
+      "+1": 0,
+      "-1": 0,
+      "laugh": 0,
+      "hooray": 0,
+      "confused": 0,
+      "heart": 0,
+      "rocket": 0,
+      "eyes": 0
+    },
+    "timeline_url": "https://api.github.com/repos/untangledco/streaming/issues/19/timeline",
+    "performed_via_github_app": null,
+    "state_reason": null
+  },
+  {
+    "url": "https://api.github.com/repos/untangledco/streaming/issues/12",
+    "repository_url": "https://api.github.com/repos/untangledco/streaming",
+    "labels_url": "https://api.github.com/repos/untangledco/streaming/issues/12/labels{/name}",
+    "comments_url": "https://api.github.com/repos/untangledco/streaming/issues/12/comments",
+    "events_url": "https://api.github.com/repos/untangledco/streaming/issues/12/events",
+    "html_url": "https://github.com/untangledco/streaming/issues/12",
+    "id": 2327047060,
+    "node_id": "I_kwDOL1KTdc6Ks-uU",
+    "number": 12,
+    "title": "m3u8: higher-level validation of Variant before returning",
+    "user": {
+      "login": "ollytom",
+      "id": 17470471,
+      "node_id": "MDQ6VXNlcjE3NDcwNDcx",
+      "avatar_url": "https://avatars.githubusercontent.com/u/17470471?v=4",
+      "gravatar_id": "",
+      "url": "https://api.github.com/users/ollytom",
+      "html_url": "https://github.com/ollytom",
+      "followers_url": "https://api.github.com/users/ollytom/followers",
+      "following_url": "https://api.github.com/users/ollytom/following{/other_user}",
+      "gists_url": "https://api.github.com/users/ollytom/gists{/gist_id}",
+      "starred_url": "https://api.github.com/users/ollytom/starred{/owner}{/repo}",
+      "subscriptions_url": "https://api.github.com/users/ollytom/subscriptions",
+      "organizations_url": "https://api.github.com/users/ollytom/orgs",
+      "repos_url": "https://api.github.com/users/ollytom/repos",
+      "events_url": "https://api.github.com/users/ollytom/events{/privacy}",
+      "received_events_url": "https://api.github.com/users/ollytom/received_events",
+      "type": "User",
+      "user_view_type": "public",
+      "site_admin": false
+    },
+    "labels": [],
+    "state": "open",
+    "locked": false,
+    "assignee": null,
+    "assignees": [],
+    "milestone": null,
+    "comments": 0,
+    "created_at": "2024-05-31T06:18:53Z",
+    "updated_at": "2024-05-31T06:18:53Z",
+    "closed_at": null,
+    "author_association": "MEMBER",
+    "type": null,
+    "active_lock_reason": null,
+    "sub_issues_summary": {
+      "total": 0,
+      "completed": 0,
+      "percent_completed": 0
+    },
+    "issue_dependencies_summary": {
+      "blocked_by": 0,
+      "total_blocked_by": 0,
+      "blocking": 0,
+      "total_blocking": 0
+    },
+    "body": "In parseVariant(), we don't do much validation beyond type checking and parsing tokens before returning a Variant.\nFor example, Apple HLS authoring spec mentions that the `CODECS` attribute must be set for all variants.\n\nThere's some easy validation. After we've broken out of the loop reading items from the lexer, have a bunch of basic if statements checking stuff:\n\n\tfunc parseVariant() {\n\t\tvar v Variant\n\t\tfor it := range items {\n\t\t\tswitch ... {\n\t\t\t}\n\t\t}\n\t\tif v.Codecs == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"codecs not set\")\n\t\t}\n\t\treturn v, nil\n\t}\n\nNot sure how to test this exactly. Two ways off the top of my head:\n\n1. make a `chan item` and send items through it. Pass the chan to parseVariant().\n2. decode simple playlists which have different \"bad\" variants in them.\n\nHowever we do it, as long as it's easy to add new, readable test cases.\nMy first instinct is to go for option 2 as it feels clearer to have bad playlist specified as text as opposed to code with structs sent through channels.",
+    "closed_by": null,
+    "reactions": {
+      "url": "https://api.github.com/repos/untangledco/streaming/issues/12/reactions",
+      "total_count": 0,
+      "+1": 0,
+      "-1": 0,
+      "laugh": 0,
+      "hooray": 0,
+      "confused": 0,
+      "heart": 0,
+      "rocket": 0,
+      "eyes": 0
+    },
+    "timeline_url": "https://api.github.com/repos/untangledco/streaming/issues/12/timeline",
+    "performed_via_github_app": null,
+    "state_reason": null
+  }
+]