diff --git a/dtos/api.go b/dtos/api.go new file mode 100644 index 0000000..c27f773 --- /dev/null +++ b/dtos/api.go @@ -0,0 +1,14 @@ +package dtos + +import "github.com/sinhamanav030/challange2015/models" + +type MovieAPIResponse struct { + Name string `json:"name"` + Cast []models.Actor `json:"cast"` + Crew []models.Actor `json:"crew"` +} + +type ActorAPIResponse struct { + Name string `json:"name"` + Movies []models.Movie `json:"movies"` +} diff --git a/dtos/seperation.go b/dtos/seperation.go new file mode 100644 index 0000000..8f33dba --- /dev/null +++ b/dtos/seperation.go @@ -0,0 +1,10 @@ +package dtos + +import ( + "github.com/sinhamanav030/challange2015/models" +) + +type SeperationDegreeResponse struct { + SeperationDegree int + Path []models.RelationNode +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d05da86 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/sinhamanav030/challange2015 + +go 1.19 diff --git a/main.go b/main.go new file mode 100644 index 0000000..640cd64 --- /dev/null +++ b/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "log" + "os" + "strings" + + "github.com/sinhamanav030/challange2015/models" + service "github.com/sinhamanav030/challange2015/service" +) + +func main() { + + args := os.Args[1:] + + if len(args) < 2 || strings.Compare(args[0], args[1]) == 0 { + log.Fatal("Incorrect: require unique source actor and dest actor url") + } + + svc := service.NewSeperationDegreeService() + + svc.Find(models.Actor{URL: args[0]}, models.Actor{URL: args[1]}) + +} diff --git a/models/actor.go b/models/actor.go new file mode 100644 index 0000000..2bd3d0a --- /dev/null +++ b/models/actor.go @@ -0,0 +1,7 @@ +package models + +type Actor struct { + Name string `json:"name"` + URL string `json:"url"` + Role string `json:"role"` +} diff --git a/models/movies.go b/models/movies.go new file mode 100644 index 0000000..f8c2d75 --- /dev/null +++ b/models/movies.go @@ -0,0 +1,7 @@ +package models + +type Movie struct { + Name string `json:"name"` + URL string `json:"url"` + Role string `json:"role"` +} diff --git a/models/queue.go b/models/queue.go new file mode 100644 index 0000000..0c58de8 --- /dev/null +++ b/models/queue.go @@ -0,0 +1,21 @@ +package models + +type QueueNode struct { + Person Actor + Role string + Path []RelationNode + IsDest bool +} + +type RelationNode struct { + Degree int + Movie string + FrstPerson Actor + SecondPerson Actor +} + +type QueueReader struct { + Queue []QueueNode + DestFound bool + ResultNode QueueNode +} diff --git a/out.txt b/out.txt new file mode 100644 index 0000000..6bdd05e --- /dev/null +++ b/out.txt @@ -0,0 +1,8 @@ +2024/10/26 10:07:25 [amitabh-bachchan robert-de-niro] +2024/10/26 10:07:25 degree: 1 +2024/10/26 10:07:55 degree: 2 +2024/10/26 10:15:04 error while fetching actor details +2024/10/26 10:17:11 error while fetching actor details +2024/10/26 10:24:11 error while fetching actor details +2024/10/26 10:24:35 degree: 3 +signal: interrupt diff --git a/service/service.go b/service/service.go new file mode 100644 index 0000000..bb92827 --- /dev/null +++ b/service/service.go @@ -0,0 +1,188 @@ +package service + +import ( + "context" + "fmt" + "log" + "strings" + "sync" + + "github.com/sinhamanav030/challange2015/models" + moviebuff "github.com/sinhamanav030/challange2015/utils/movieBuff" + mutexMap "github.com/sinhamanav030/challange2015/utils/mutexMap" +) + +type SeperationDegree interface { + Find(source, dest models.Actor) +} + +func NewSeperationDegreeService() SeperationDegree { + return &seperationDegreeService{ + client: moviebuff.NewClient(), + logger: *log.Default(), + } +} + +type seperationDegreeService struct { + client moviebuff.Client + logger log.Logger +} + +func (s *seperationDegreeService) Find(source, dest models.Actor) { + queue := []models.QueueNode{} + visited := mutexMap.NewCallVisitedMutex() + sourceNode := models.QueueNode{ + Person: source, + } + visited.Set(source.URL) + queue = append(queue, sourceNode) + + degree := 1 + + for len(queue) > 0 { + ctx, cancelCtx := context.WithCancel(context.Background()) + + queueWg := &sync.WaitGroup{} + queueChan := make(chan models.QueueReader, 1) + + nodeJobChan := make(chan models.QueueNode, len(queue)) + nodeResChan := make(chan models.QueueNode) + + queueWg.Add(1) + go func() { + defer queueWg.Done() + nxtLvlQueue := []models.QueueNode{} + for node := range nodeResChan { + if node.IsDest { + queueChan <- models.QueueReader{DestFound: true, ResultNode: node} + cancelCtx() + return + } + nxtLvlQueue = append(nxtLvlQueue, node) + } + queueChan <- models.QueueReader{Queue: nxtLvlQueue} + }() + + queueWg.Add(1) + go func() { + defer queueWg.Done() + wg := &sync.WaitGroup{} + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + s.traverse(ctx, nodeResChan, nodeJobChan, source, dest, visited) + }() + } + for _, node := range queue { + nodeJobChan <- node + } + close(nodeJobChan) + + wg.Wait() + + close(nodeResChan) + }() + + queueWg.Wait() + close(queueChan) + + res := <-queueChan + + if res.DestFound { + s.printRelation(res.ResultNode, degree) + return + } + + queue = res.Queue + degree += 1 + } + + s.printRelation(models.QueueNode{}, 0) +} + +func (s *seperationDegreeService) traverse(ctx context.Context, nodeResChn chan<- models.QueueNode, nodeJobChn <-chan models.QueueNode, source models.Actor, dest models.Actor, visited *mutexMap.CallVisitedMutex) { + for curNode := range nodeJobChn { + if len(curNode.Person.URL) == 0 { + s.logger.Print("Skipping Invalid Node") + continue + } + + resp, err := s.client.FetchActor(curNode.Person.URL) + if err != nil { + s.logger.Print("error while fetching actor details") + fmt.Println("here :", curNode.Person.URL) + continue + } + + for _, movie := range resp.Movies { + if visited.Get(movie.URL) { + continue + } + visited.Set(movie.URL) + + movieDetails, err := s.client.FetchMovie(movie.URL) + if err != nil || movieDetails == nil { + s.logger.Print("error while fetching movie details") + continue + } + + movieDetails.Cast = append(movieDetails.Cast, movieDetails.Crew...) + for _, actor := range movieDetails.Cast { + if strings.Compare(source.URL, curNode.Person.URL) == 0 && strings.Compare(curNode.Person.URL, actor.URL) == 0 { + curNode.Person.Name = actor.Name + curNode.Person.Role = actor.Role + } + + if visited.Get(actor.URL) { + continue + } + + relationNode := models.RelationNode{ + FrstPerson: curNode.Person, + SecondPerson: actor, + Movie: movie.Name, + } + + node := models.QueueNode{ + Person: actor, + } + + if strings.Compare(dest.URL, actor.URL) == 0 { + node.IsDest = true + } + + node.Path = append(node.Path, curNode.Path...) + node.Path = append(node.Path, relationNode) + + visited.Set(actor.URL) + + select { + case nodeResChn <- node: + case <-ctx.Done(): + return + } + + if node.IsDest { + return + } + + } + } + } +} + +func (s *seperationDegreeService) printRelation(node models.QueueNode, degree int) { + if degree == 0 { + fmt.Println("\nNo Relation found.") + return + } + + fmt.Println("\nDegree of Seperation: ", degree) + for i, relation := range node.Path { + fmt.Printf("%d. Movie: %s\n", i+1, relation.Movie) + fmt.Printf("%s: %s\n", relation.FrstPerson.Role, relation.FrstPerson.Name) + fmt.Printf("%s: %s\n", relation.SecondPerson.Role, relation.SecondPerson.Name) + fmt.Println() + } +} diff --git a/utils/movieBuff/client.go b/utils/movieBuff/client.go new file mode 100644 index 0000000..5fc2351 --- /dev/null +++ b/utils/movieBuff/client.go @@ -0,0 +1,70 @@ +package moviebuff + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "time" + + "github.com/sinhamanav030/challange2015/dtos" +) + +type Client interface { + FetchActor(actor string) (*dtos.ActorAPIResponse, error) + FetchMovie(movie string) (*dtos.MovieAPIResponse, error) +} + +type client struct { + httpClient *http.Client + logger log.Logger +} + +func NewClient() Client { + client := &client{ + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + logger: *log.Default(), + } + return client +} + +func (c *client) makeHttpReq(suffix string) (*http.Response, error) { + httpUrl := fmt.Sprintf("https://data.moviebuff.com/%s", suffix) + req, err := http.NewRequest("GET", httpUrl, nil) + if err != nil { + return nil, err + } + res, err := c.httpClient.Do(req) + return res, err +} + +func (c *client) FetchActor(actor string) (*dtos.ActorAPIResponse, error) { + res, err := c.makeHttpReq(actor) + if err != nil { + return nil, err + } + defer res.Body.Close() + var data dtos.ActorAPIResponse + if res.StatusCode != 200 { + return &data, nil + } + + err = json.NewDecoder(res.Body).Decode(&data) + return &data, err +} + +func (c *client) FetchMovie(movie string) (*dtos.MovieAPIResponse, error) { + res, err := c.makeHttpReq(movie) + if err != nil { + return nil, err + } + defer res.Body.Close() + var data dtos.MovieAPIResponse + if res.StatusCode != 200 { + return &data, nil + } + err = json.NewDecoder(res.Body).Decode(&data) + return &data, err +} diff --git a/utils/mutexMap/map.go b/utils/mutexMap/map.go new file mode 100644 index 0000000..aedffad --- /dev/null +++ b/utils/mutexMap/map.go @@ -0,0 +1,30 @@ +package mutexmap + +import "sync" + +type CallVisitedMutex struct { + sync.Mutex + visited map[string]bool +} + +func NewCallVisitedMutex() *CallVisitedMutex { + c := CallVisitedMutex{ + visited: make(map[string]bool), + } + + return &c +} + +func (c *CallVisitedMutex) Set(name string) { + c.Lock() + c.visited[name] = true + c.Unlock() +} + +func (c *CallVisitedMutex) Get(name string) bool { + c.Lock() + set := c.visited[name] + c.Unlock() + + return set +}