@@ -167,24 +167,40 @@ func updateServerInfo(server serverWithName) error {
167167
168168 // Extract owner and repo from repository URL
169169 var newStars , newPulls int
170+
171+ // Get GitHub stars if we have a repository URL
170172 if repoURL != "" {
171173 owner , repo , err := extractOwnerRepo (repoURL )
172174 if err != nil {
173175 logger .Warnf ("Failed to extract owner/repo from URL %s: %v" , repoURL , err )
176+ newStars = currentStars
174177 } else {
175178 // Get repository info from GitHub API
176- stars , pulls , err := getGitHubRepoInfo (owner , repo , server .name , currentPulls )
179+ stars , _ , err := getGitHubRepoInfo (owner , repo , server .name , currentPulls )
177180 if err != nil {
178181 logger .Warnf ("Failed to get GitHub repo info for %s: %v" , server .name , err )
179182 newStars = currentStars
180- newPulls = currentPulls
181183 } else {
182184 newStars = stars
183- newPulls = pulls
184185 }
185186 }
186187 } else {
187188 newStars = currentStars
189+ }
190+
191+ // Get container pull count if we have an image
192+ if server .entry .Image != "" {
193+ pullCount , err := getContainerPullCount (server .entry .Image )
194+ if err != nil {
195+ logger .Warnf ("Failed to get pull count for image %s: %v" , server .entry .Image , err )
196+ newPulls = currentPulls
197+ } else if pullCount > 0 {
198+ newPulls = pullCount
199+ } else {
200+ // No pull count available (GHCR or private registry)
201+ newPulls = currentPulls
202+ }
203+ } else {
188204 newPulls = currentPulls
189205 }
190206
@@ -376,7 +392,7 @@ func extractOwnerRepo(url string) (string, string, error) {
376392 return owner , repo , nil
377393}
378394
379- // getGitHubRepoInfo gets the stars and downloads count for a GitHub repository
395+ // getGitHubRepoInfo gets the stars count for a GitHub repository
380396func getGitHubRepoInfo (owner , repo , serverName string , currentPulls int ) (stars int , pulls int , err error ) {
381397 // Create HTTP client with timeout
382398 client := & http.Client {
@@ -417,10 +433,193 @@ func getGitHubRepoInfo(owner, repo, serverName string, currentPulls int) (stars
417433 return 0 , 0 , fmt .Errorf ("failed to parse response: %w" , err )
418434 }
419435
420- // For pulls/downloads, increment by a small amount
421- // In a real implementation, you would query Docker Hub API for actual pull counts
422- increment := 50 + (len (serverName ) % 100 )
423- pulls = currentPulls + increment
436+ // Return current pulls - we'll fetch container pulls separately
437+ return repoInfo .StargazersCount , currentPulls , nil
438+ }
439+
440+ // getContainerPullCount fetches the pull count for a container image
441+ func getContainerPullCount (image string ) (int , error ) {
442+ // Parse the image reference
443+ parts := strings .Split (image , ":" )
444+ if len (parts ) < 1 {
445+ return 0 , fmt .Errorf ("invalid image format: %s" , image )
446+ }
447+
448+ imageName := parts [0 ]
449+
450+ // Determine registry and fetch accordingly
451+ if strings .HasPrefix (imageName , "ghcr.io/" ) {
452+ return getGHCRPullCount (imageName )
453+ } else if strings .Contains (imageName , "/" ) && ! strings .Contains (imageName , "." ) {
454+ // Likely Docker Hub (no dots in the hostname part)
455+ return getDockerHubPullCount (imageName )
456+ }
457+
458+ // Unknown registry, return 0
459+ logger .Warnf ("Unknown registry for image %s, cannot fetch pull count" , image )
460+ return 0 , nil
461+ }
462+
463+ // getGHCRPullCount fetches pull count for GitHub Container Registry images
464+ func getGHCRPullCount (imageName string ) (int , error ) {
465+ // GHCR requires authentication to get package statistics
466+ if githubToken == "" {
467+ logger .Debugf ("No GitHub token available, cannot fetch GHCR pull count for %s" , imageName )
468+ return 0 , nil
469+ }
470+
471+ // Parse the image name: ghcr.io/owner/repo/package or ghcr.io/owner/package
472+ imageName = strings .TrimPrefix (imageName , "ghcr.io/" )
473+ parts := strings .Split (imageName , "/" )
474+ if len (parts ) < 2 {
475+ return 0 , fmt .Errorf ("invalid GHCR image format: %s" , imageName )
476+ }
477+
478+ owner := parts [0 ]
479+ // The package name is everything after the owner
480+ packageName := strings .Join (parts [1 :], "/" )
481+
482+ // GitHub Packages API endpoint for container packages
483+ // Note: This requires the package to be public or the token to have package:read scope
484+ url := fmt .Sprintf ("https://api.github.com/users/%s/packages/container/%s" , owner , packageName )
485+
486+ client := & http.Client {
487+ Timeout : 10 * time .Second ,
488+ }
489+
490+ req , err := http .NewRequestWithContext (context .Background (), "GET" , url , nil )
491+ if err != nil {
492+ // Try org endpoint if user endpoint fails
493+ url = fmt .Sprintf ("https://api.github.com/orgs/%s/packages/container/%s" , owner , packageName )
494+ req , err = http .NewRequestWithContext (context .Background (), "GET" , url , nil )
495+ if err != nil {
496+ return 0 , fmt .Errorf ("failed to create request: %w" , err )
497+ }
498+ }
499+
500+ req .Header .Add ("Accept" , "application/vnd.github.v3+json" )
501+ req .Header .Add ("Authorization" , "token " + githubToken )
502+
503+ resp , err := client .Do (req )
504+ if err != nil {
505+ return 0 , fmt .Errorf ("failed to send request: %w" , err )
506+ }
507+ defer resp .Body .Close ()
508+
509+ if resp .StatusCode == http .StatusNotFound {
510+ // Try org endpoint if user endpoint returned 404
511+ if strings .Contains (url , "/users/" ) {
512+ url = strings .Replace (url , "/users/" , "/orgs/" , 1 )
513+ req , err = http .NewRequestWithContext (context .Background (), "GET" , url , nil )
514+ if err != nil {
515+ return 0 , fmt .Errorf ("failed to create request: %w" , err )
516+ }
517+
518+ req .Header .Add ("Accept" , "application/vnd.github.v3+json" )
519+ req .Header .Add ("Authorization" , "token " + githubToken )
520+
521+ resp , err = client .Do (req )
522+ if err != nil {
523+ return 0 , fmt .Errorf ("failed to send request: %w" , err )
524+ }
525+ defer resp .Body .Close ()
526+ }
527+ }
528+
529+ if resp .StatusCode != http .StatusOK {
530+ // Package not found or no access - return 0
531+ logger .Debugf ("Could not fetch GHCR package stats (status %d) for %s" , resp .StatusCode , imageName )
532+ return 0 , nil
533+ }
534+
535+ // GitHub Packages API doesn't directly expose download count in the package endpoint
536+ // We need to get package versions and sum their download counts
537+ var packageInfo struct {
538+ ID int `json:"id"`
539+ Name string `json:"name"`
540+ }
541+
542+ if err := json .NewDecoder (resp .Body ).Decode (& packageInfo ); err != nil {
543+ return 0 , fmt .Errorf ("failed to parse response: %w" , err )
544+ }
545+
546+ // Now get all versions to sum download counts
547+ versionsURL := fmt .Sprintf ("%s/versions?per_page=100" , url )
548+ req , err = http .NewRequestWithContext (context .Background (), "GET" , versionsURL , nil )
549+ if err != nil {
550+ return 0 , fmt .Errorf ("failed to create versions request: %w" , err )
551+ }
552+
553+ req .Header .Add ("Accept" , "application/vnd.github.v3+json" )
554+ req .Header .Add ("Authorization" , "token " + githubToken )
555+
556+ resp , err = client .Do (req )
557+ if err != nil {
558+ return 0 , fmt .Errorf ("failed to send versions request: %w" , err )
559+ }
560+ defer resp .Body .Close ()
561+
562+ if resp .StatusCode != http .StatusOK {
563+ logger .Debugf ("Could not fetch GHCR package versions (status %d) for %s" , resp .StatusCode , imageName )
564+ return 0 , nil
565+ }
566+
567+ var versions []struct {
568+ Metadata struct {
569+ Container struct {
570+ Tags []string `json:"tags"`
571+ } `json:"container"`
572+ } `json:"metadata"`
573+ // Unfortunately, GitHub API doesn't expose download_count for container packages
574+ // in the same way it does for other package types
575+ }
576+
577+ if err := json .NewDecoder (resp .Body ).Decode (& versions ); err != nil {
578+ return 0 , fmt .Errorf ("failed to parse versions response: %w" , err )
579+ }
580+
581+ // GitHub doesn't expose container download counts through the API
582+ // even with authentication. This is a known limitation.
583+ // Return 0 to indicate we couldn't get the data
584+ logger .Debugf ("GHCR package found but download count not available through API for %s" , imageName )
585+ return 0 , nil
586+ }
424587
425- return repoInfo .StargazersCount , pulls , nil
588+ // getDockerHubPullCount fetches pull count for Docker Hub images
589+ func getDockerHubPullCount (imageName string ) (int , error ) {
590+ // Remove docker.io prefix if present
591+ imageName = strings .TrimPrefix (imageName , "docker.io/" )
592+
593+ // Docker Hub API endpoint
594+ url := fmt .Sprintf ("https://hub.docker.com/v2/repositories/%s/" , imageName )
595+
596+ client := & http.Client {
597+ Timeout : 10 * time .Second ,
598+ }
599+
600+ req , err := http .NewRequestWithContext (context .Background (), "GET" , url , nil )
601+ if err != nil {
602+ return 0 , fmt .Errorf ("failed to create request: %w" , err )
603+ }
604+
605+ resp , err := client .Do (req )
606+ if err != nil {
607+ return 0 , fmt .Errorf ("failed to send request: %w" , err )
608+ }
609+ defer resp .Body .Close ()
610+
611+ if resp .StatusCode != http .StatusOK {
612+ // Not found or error - return 0
613+ return 0 , nil
614+ }
615+
616+ var dockerHubResp struct {
617+ PullCount int `json:"pull_count"`
618+ }
619+
620+ if err := json .NewDecoder (resp .Body ).Decode (& dockerHubResp ); err != nil {
621+ return 0 , fmt .Errorf ("failed to parse response: %w" , err )
622+ }
623+
624+ return dockerHubResp .PullCount , nil
426625}
0 commit comments