Add channels

This commit is contained in:
Tim Van Baak 2025-01-31 17:23:41 -08:00
parent 3f533d568a
commit e40e2a3245
9 changed files with 247 additions and 29 deletions

View File

@ -7,7 +7,10 @@ import (
var channelCmd = &cobra.Command{
Use: "channel",
Short: "Manage channels",
Long: `
Long: `Manage channels.
A channel is a group of sources that can be viewed together. Adding a source
to a channel creates it and removing all sources from a channel deletes it.
`,
}

View File

@ -3,19 +3,43 @@ package cmd
import (
"log"
"github.com/Jaculabilis/intake/core"
"github.com/spf13/cobra"
)
var channelAddCmd = &cobra.Command{
Use: "add",
Short: "Create a channel",
Long: `
Short: "Add a source to a channel",
Long: `Add a source to a channel.
`,
Run: func(cmd *cobra.Command, args []string) {
log.Fatal("not implemented")
channelAdd(stringArg(cmd, "channel"), stringArg(cmd, "source"))
},
}
func init() {
channelCmd.AddCommand(channelAddCmd)
channelAddCmd.Flags().StringP("channel", "c", "", "Channel name")
channelAddCmd.MarkFlagRequired("channel")
channelAddCmd.Flags().StringP("source", "s", "", "Source to add")
channelAddCmd.MarkFlagRequired("source")
}
func channelAdd(channel string, source string) {
if channel == "" {
log.Fatal("error: --channel is empty")
}
if source == "" {
log.Fatal("error: --source is empty")
}
db := openAndMigrateDb()
if err := core.AddSourceToChannel(db, channel, source); err != nil {
log.Fatalf("error: failed to add source to channel: %v", err)
}
log.Printf("Added source %s to channel %s", source, channel)
}

View File

@ -3,19 +3,44 @@ package cmd
import (
"log"
"github.com/Jaculabilis/intake/core"
"github.com/spf13/cobra"
)
var channelDeleteCmd = &cobra.Command{
Use: "delete",
Short: "Delete a channel",
Long: `
Use: "remove",
Aliases: []string{"rm"},
Short: "Remove a source from a channel",
Long: `Remove a source from a channel.
`,
Run: func(cmd *cobra.Command, args []string) {
log.Fatal("not implemented")
channelRemove(stringArg(cmd, "channel"), stringArg(cmd, "source"))
},
}
func init() {
channelCmd.AddCommand(channelDeleteCmd)
channelDeleteCmd.Flags().StringP("channel", "c", "", "Channel name")
channelDeleteCmd.MarkFlagRequired("channel")
channelDeleteCmd.Flags().StringP("source", "s", "", "Source to add")
channelDeleteCmd.MarkFlagRequired("source")
}
func channelRemove(channel string, source string) {
if channel == "" {
log.Fatal("error: --channel is empty")
}
if source == "" {
log.Fatal("error: --source is empty")
}
db := openAndMigrateDb()
if err := core.DeleteSourceFromChannel(db, channel, source); err != nil {
log.Fatalf("error: failed to remove source from channel: %v", err)
}
log.Printf("Removed source %s from channel %s", source, channel)
}

View File

@ -1,21 +0,0 @@
package cmd
import (
"log"
"github.com/spf13/cobra"
)
var channelEditCmd = &cobra.Command{
Use: "edit",
Short: "Edit a channel",
Long: `
`,
Run: func(cmd *cobra.Command, args []string) {
log.Fatal("not implemented")
},
}
func init() {
channelCmd.AddCommand(channelEditCmd)
}

45
cmd/channelList.go Normal file
View File

@ -0,0 +1,45 @@
package cmd
import (
"fmt"
"log"
"github.com/Jaculabilis/intake/core"
"github.com/spf13/cobra"
)
var channelListCmd = &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List channels",
Long: `List channels.
`,
Run: func(cmd *cobra.Command, args []string) {
channelList(stringArg(cmd, "channel"))
},
}
func init() {
channelCmd.AddCommand(channelListCmd)
channelListCmd.Flags().StringP("channel", "c", "", "List sources in a specific channel")
}
func channelList(channel string) {
db := openAndMigrateDb()
channelSources, err := core.GetSourcesInChannel(db)
if err != nil {
log.Fatalf("error: failed to get sources in channel: %v", err)
}
if channel == "" {
for channel := range channelSources {
fmt.Println(channel)
}
} else {
for _, source := range channelSources[channel] {
fmt.Println(source)
}
}
}

41
core/channel.go Normal file
View File

@ -0,0 +1,41 @@
package core
func AddSourceToChannel(db DB, channel string, source string) error {
_, err := db.Exec(`
insert into channels (name, source)
values (?, ?)
`, channel, source)
return err
}
func DeleteSourceFromChannel(db DB, channel string, source string) error {
_, err := db.Exec(`
delete from channels
where name = ? and source = ?
`, channel, source)
return err
}
func GetSourcesInChannel(db DB) (map[string][]string, error) {
rows, err := db.Query(`
select name, source
from channels
order by name, source
`)
if err != nil {
return nil, err
}
defer rows.Close()
channelSources := make(map[string][]string)
for rows.Next() {
var name, source string
if err := rows.Scan(&name, &source); err != nil {
return nil, err
}
channelSources[name] = append(channelSources[name], source)
}
if err := rows.Err(); err != nil {
return nil, err
}
return channelSources, nil
}

70
core/channel_test.go Normal file
View File

@ -0,0 +1,70 @@
package core
import "testing"
func TestChannel(t *testing.T) {
db := EphemeralDb(t)
if err := AddSource(db, "one"); err != nil {
t.Fatalf("failed to add source: %v", err)
}
if err := AddSource(db, "two"); err != nil {
t.Fatalf("failed to add source: %v", err)
}
// Add sources to channel
if err := AddSourceToChannel(db, "channel", "one"); err != nil {
t.Fatalf("failed to add source to channel: %v", err)
}
if err := AddSourceToChannel(db, "channel", "two"); err != nil {
t.Fatalf("failed to add source to channel: %v", err)
}
// Both sources are in the channel
sources, err := GetSourcesInChannel(db)
if err != nil {
t.Fatalf("failed to get sources in channel: %v", err)
}
if len(sources["channel"]) != 2 || sources["channel"][0] != "one" || sources["channel"][1] != "two" {
t.Fatalf("expected two sources, got %d: %v", len(sources), sources)
}
// Get sources in channel after deletion
if err := DeleteSourceFromChannel(db, "channel", "one"); err != nil {
t.Fatalf("failed to delete source from channel: %v", err)
}
sources, err = GetSourcesInChannel(db)
if err != nil {
t.Fatalf("failed to get sources in channel: %v", err)
}
if len(sources) != 1 || sources["channel"][0] != "two" {
t.Fatalf("unexpected sources in channel after deletion: %v", sources)
}
if err := AddSourceToChannel(db, "channel", "one"); err != nil {
t.Fatalf("failed to add source to channel: %v", err)
}
// Items on both sources appear in the channel
if err := AddItems(db, []Item{
{"one", "a", 0, true, "", "", "", "", 0, nil},
{"two", "b", 0, true, "", "", "", "", 0, nil},
}); err != nil {
t.Fatalf("failed to add items to one: %v", err)
}
if _, err := DeactivateItem(db, "one", "a"); err != nil {
t.Fatalf("failed to deactivate item: %v", err)
}
items, err := GetAllItemsForChannel(db, "channel")
if err != nil {
t.Fatalf("failed to get all items in channel: %v", err)
}
if len(items) != 2 || items[0].Id != "a" || items[1].Id != "b" {
t.Fatalf("expected two items, got %d: %v", len(items), items)
}
items, err = GetActiveItemsForChannel(db, "channel")
if err != nil {
t.Fatalf("failed to get all items in channel: %v", err)
}
if len(items) != 1 || items[0].Id != "b" {
t.Fatalf("expected one item, got %d: %v", len(items), items)
}
}

View File

@ -202,3 +202,28 @@ func GetAllItemsForSource(db DB, source string) ([]Item, error) {
order by case when time = 0 then created else time end, id
`, source)
}
func GetActiveItemsForChannel(db DB, channel string) ([]Item, error) {
return getItems(db, `
select
i.source, i.id, i.created, i.active, i.title, i.author, i.body, i.link, i.time, json(i.action)
from items i
join channels c on i.source = c.source
where
c.name = ?
and i.active <> 0
order by case when i.time = 0 then i.created else i.time end, i.id
`, channel)
}
func GetAllItemsForChannel(db DB, channel string) ([]Item, error) {
return getItems(db, `
select
i.source, i.id, i.created, i.active, i.title, i.author, i.body, i.link, i.time, json(i.action)
from items i
join channels c on i.source = c.source
where
c.name = ?
order by case when i.time = 0 then i.created else i.time end, i.id
`, channel)
}

View File

@ -31,3 +31,9 @@ create table items(
primary key (source, id),
foreign key (source) references sources (name) on delete cascade
) strict;
create table channels(
name text not null,
source text not null,
unique (name, source) on conflict replace
foreign key (source) references sources (name) on delete cascade
)